diff --git a/.github/get_min_dependencies.py b/.github/get_min_dependencies.py new file mode 100644 index 0000000000..72dce32f17 --- /dev/null +++ b/.github/get_min_dependencies.py @@ -0,0 +1,15 @@ +"""This script fetches minimum dependencies of cleanlab package and writes them to the file requirements-min.txt""" +import json + + +if __name__ == "__main__": + with open("./deps.json", "r") as f: + deps = json.load(f) + + for package in deps: + if package["package"]["package_name"] == "cleanlab": + for dep in package["dependencies"]: + req_version = dep["required_version"] + with open("requirements-min.txt", "a") as f: + if req_version.startswith(">="): + f.write(f"{dep['package_name']}=={req_version[2:]}\n") diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0896e2a082..e586395a36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,53 @@ on: schedule: - cron: '0 12 * * 1' jobs: + test37: + name: "Test: Python 3.7 on ${{ matrix.os }}" + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - macos-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.7.16 + - name: Install cleanlab + run: pip install -e . + - name: Install development dependencies + run: pip install -r requirements-dev.txt + - name: Install fasttext for non-Windows machines + if: matrix.os != 'windows-latest' + run: | + pip install fasttext + - name: Test with coverage + run: pytest --verbose --cov=cleanlab/ --cov-config .coveragerc --cov-report=xml + env: + TEST_FASTTEXT: true + - uses: codecov/codecov-action@v3 + test37-windows: + name: "Test: Python 3.7 on windows" + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.7.9 + - name: Install cleanlab + run: pip install -e . + - name: Install development dependencies + run: pip install -r requirements-dev.txt + - name: Overwrite tensorflow version on Windows + run: | + pip uninstall -y tensorflow + pip install tensorflow-cpu + - name: Test with coverage + run: pytest --verbose --cov=cleanlab/ --cov-config .coveragerc --cov-report=xml + env: + TEST_FASTTEXT: true + - uses: codecov/codecov-action@v3 test: name: "Test: Python ${{ matrix.python }} on ${{ matrix.os }}" runs-on: ${{ matrix.os }} @@ -15,7 +62,6 @@ jobs: - macos-latest - windows-latest python: - - "3.7" - "3.8" - "3.9" - "3.10" @@ -26,10 +72,12 @@ jobs: python-version: ${{ matrix.python }} - name: Install cleanlab run: pip install -e . - - name: Check cleanlab runs without optional dependencies - run: python3 -c "import cleanlab" - name: Install development dependencies run: pip install -r requirements-dev.txt + - name: Install fasttext for non-Windows machines + if: matrix.os != 'windows-latest' + run: | + pip install fasttext - name: Overwrite tensorflow version on Windows if: matrix.os == 'windows-latest' run: | @@ -37,7 +85,32 @@ jobs: pip install tensorflow-cpu - name: Test with coverage run: pytest --verbose --cov=cleanlab/ --cov-config .coveragerc --cov-report=xml - - uses: codecov/codecov-action@v2 + env: + TEST_FASTTEXT: true + - uses: codecov/codecov-action@v3 + test-without-extras-min-versions: + name: Test without optional dependencies and with minimum compatible versions of dependencies + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.7' + - name: Install cleanlab + run: | + python -m pip install --upgrade pip + pip install . + - name: Install test dependencies + run: | + pip install pytest pytest-lazy-fixture pipdeptree + pipdeptree -j > deps.json + - name: Install minimum versions + run: | + python ./.github/get_min_dependencies.py + pip install -r requirements-min.txt + - name: Run tests + run: | + pytest tests/test_multilabel_classification.py tests/test_multiannotator.py tests/test_filter_count.py typecheck: name: Type check runs-on: ubuntu-latest diff --git a/.github/workflows/gh-pages.yaml b/.github/workflows/gh-pages.yaml index a1f2f84fa7..ae0ce12e53 100644 --- a/.github/workflows/gh-pages.yaml +++ b/.github/workflows/gh-pages.yaml @@ -28,12 +28,12 @@ jobs: sudo tar xzvf pandoc-2.19.2-linux-amd64.tar.gz --strip-components 1 -C /usr/local - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: "3.10" - name: Setup Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: "16" @@ -45,7 +45,7 @@ jobs: run: echo "::set-output name=dir::$(pip cache dir)" - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml index fba7abc18c..488bea1b45 100644 --- a/.github/workflows/links.yml +++ b/.github/workflows/links.yml @@ -12,15 +12,18 @@ jobs: - run: >- sudo apt-get install -y pandoc - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - run: | find . -name '*.html' -delete - run: | find . -name '*.md' -exec pandoc -i {} -o {}.html \; - - uses: anishathalye/proof-html@v1 + - uses: anishathalye/proof-html@v2 with: directory: . + check_html: false check_favicon: false - empty_alt_ignore: true - url_ignore_re: | - ^https:\/\/docs\.github\.com\/ + ignore_missing_alt: true + tokens: | + {"https://github.com": "${{ secrets.GITHUB_TOKEN }}"} + swap_urls: | + {"^\.\/\(.*\).md": "\\1.md.html"} diff --git a/.gitignore b/.gitignore index 16915dde28..e46bc2dfb5 100644 --- a/.gitignore +++ b/.gitignore @@ -118,6 +118,8 @@ venv.bak/ /docs/source/notebooks/*.gz /docs/source/notebooks/spoken_digits /docs/source/notebooks/pretrained_models +/docs/source/tutorials/datalab/datalab-files/ -# VS Code +# Editor files .vscode/ +.idea/ diff --git a/.mypy.ini b/.mypy.ini index ab7155b9e2..c119d9c9e1 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -21,3 +21,12 @@ ignore_missing_imports = True [mypy-tqdm.*] ignore_missing_imports = True + +[mypy-matplotlib.*] +ignore_missing_imports = True + +[mypy-datasets.*] +ignore_missing_imports = True + +[mypy-scipy.*] +ignore_missing_imports = True diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 80063408ba..d88a2e3f1e 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -13,9 +13,9 @@ and [venv](https://docs.python.org/3/library/venv.html). You can the tools and choose what is right for you. Here, we'll explain how to get set up with venv, which is built in to Python 3. -```console -$ python3 -m venv ./ENV # create a new virtual environment in the directory ENV -$ source ./ENV/bin/activate # switch to using the virtual environment +```shell +python3 -m venv ./ENV # create a new virtual environment in the directory ENV +source ./ENV/bin/activate # switch to using the virtual environment ``` You only need to create the virtual environment once, but you will need to @@ -27,37 +27,125 @@ virtual environment rather than your system Python installation. Run the following commands in the repository's root directory. -1. Install development requirements with `pip install -r requirements-dev.txt` +1. Install development requirements +```shell +pip install -r requirements-dev.txt +``` -2. Install cleanlab as an editable package with `pip install -e .` +2. Install cleanlab as an editable package +```shell +pip install -e . +``` For Macs with Apple silicon: replace `tensorflow` in requirements-dev.txt with: `tensorflow-macos==2.9.2` and `tensorflow-metal==0.5.1` +### Handling optional dependencies + +When designing a class that relies on an optional, domain-specific runtime dependency, it is better to use lazy-importing to avoid forcing users to install the dependency if they do not need it. + +Depending on the coupling of your class to the dependency, you may want to consider importing it at the module-level or as an instance variable of the class or a function that uses the dependency. + +If the dependency is used by many methods in the module or other classes, it is better to import it at the module-level. +On the other hand, if the dependency is only used by a handful of methods, then it's better to import it inside the method. If the dependency is not installed, an ImportError should be raised when the method is called, along with instructions on how to install the dependency. + +Here is an example of a class that lazily imports CuPy and has a sum method (element-wise) that can be used on both CPU and GPU devices. + +Unless an alternative implementations of the sum method is available, an `ImportError` should be raised when the method is called with instructions on how to install the dependency. + +
Example code + +```python +def lazy_import_cupy(): + try: + import cupy + except ImportError as error: + # If the dependency is required for the class to work, + # replace this block with a raised ImportError containing instructions + print("Warning: cupy is not installed. Please install it with `pip install cupy`.") + cupy = None + return cupy + +class Summation: + def __init__(self): + self.cupy = lazy_import_cupy() + def sum(self, x) -> float: + if self.cupy is None: + return sum(x) + return self.cupy.sum(x) +``` +
+ + +For the build system to recognize the optional dependency, you should add it to the `EXTRAS_REQUIRE` constant in **setup.py**: + +
Example code + +```python +EXTRAS_REQUIRE = { + ... + "gpu": [ + # Explain why the dependency below is needed, + # e.g. "for performing summation on GPU" + "cupy", + ], +} +``` + + +Or assign to a separate variable and add it to `EXTRAS_REQUIRE` + +```python +GPU_REQUIRES = [ + # Explanation ... + "cupy", +] + +EXTAS_REQUIRE = { + ... + "gpu": GPU_REQUIRES, +} +``` +
+ + +The package can be installed with the optional dependency (here called `gpu`) via: + +1. PyPI installation + +```shell +pip install -r "cleanlab[gpu]" +``` + +2. Editable installation + +```shell +pip install -e ".[gpu]" +``` ## Testing **Run all the tests:** -```console -$ pytest +```shell +pytest ``` **Run a specific file or test:** -``` -$ pytest -k +```shell +pytest -k ``` **Run with verbose output:** -``` -$ pytest --verbose +```shell +pytest --verbose ``` **Run with code coverage:** -``` -$ pytest --cov=cleanlab/ --cov-config .coveragerc --cov-report=html +```shell +pytest --cov=cleanlab/ --cov-config .coveragerc --cov-report=html ``` The coverage report will be available in `coverage_html_report/index.html`, @@ -69,13 +157,13 @@ Cleanlab uses [mypy](https://mypy.readthedocs.io/en/stable/) typing. Type checki **Check typing in all files:** -``` -$ mypy cleanlab +```shell +mypy cleanlab ``` The above is just a simplified command for demonstration, do NOT run this for testing your own type annotations! Our CI adds a few additional flags to the `mypy` command it uses in the file: -**.github/workflows/ci.yml**. +**.github/workflows/ci.yml**. To exactly match the `mypy` command that is executed in CI, copy these flags, and also ensure your version of `mypy` and related packages like `pandas-stubs` match the latest released versions (used in our CI). ### Examples @@ -84,7 +172,7 @@ You can check that the [examples](https://github.com/cleanlab/examples) still work with changes you make to cleanlab by manually running the notebooks. You can also run all example notebooks as follows: -```console +```shell git clone https://github.com/cleanlab/examples.git ``` @@ -93,27 +181,29 @@ E.g. you can edit this line to point to your local version of cleanlab as a rela Finally execute the bash script: -```console +```shell examples/run_all_notebooks.sh ``` ## How to style new code contributions -cleanlab follows the [Black](https://black.readthedocs.io/) code style. This is +cleanlab follows the [Black](https://black.readthedocs.io/) code style (see [pyproject.toml](pyproject.toml)). This is enforced by CI, so please format your code by invoking `black` before submitting a pull request. -Generally aim to follow the [PEP-8 coding style](https://peps.python.org/pep-0008/). +Generally aim to follow the [PEP-8 coding style](https://peps.python.org/pep-0008/). Please do not use wildcard `import *` in any files, instead you should always import the specific functions that you need from a module. +All cleanlab code should have a maximum line length of 100 characters. + ### Pre-commit hook This repo uses the [pre-commit framework](https://pre-commit.com/) to easily set up code style checks that run automatically whenever you make a commit. You can install the git hook scripts with: -```console -$ pre-commit install +```shell +pre-commit install ``` ### EditorConfig @@ -138,7 +228,7 @@ endings match the project style. ## Documentation You can build the docs from your local cleanlab version by following [these -instructions](docs/README.md#build-the-cleanlab-docs-locally). +instructions](./docs/README.md#build-the-cleanlab-docs-locally). If editing existing docs or adding new tutorials, please first read through our [guidelines](https://github.com/cleanlab/cleanlab/tree/master/docs#tips-for-editing-docstutorials). @@ -203,10 +293,20 @@ Try to adhere to this standardized terminology unless you have good reason not t Use relative linking to connect information between docs and jupyter notebooks, and make sure links will remain valid in the future as new cleanlab versions are released! Sphinx/html works with relative paths so try to specify relative paths if necessary. For specific situations: -- Link another function from within a source code docstring: ``:py:func:`function_name ` `` -- Link another class from within a source code docstring: ``:py:class:`class_name ` `` -- Link a tutorial notebook from within a source code docstring: ``:ref:`notebook_name ` `` -- Link a function from within a tutorial notebook: `[function_name](../cleanlab/file.rst#cleanlab.file.function_name)` +- Link another function or class from within a source code docstring: + - If you just want to specify the function/class name (ie. the function/class is unique throughout our library): `` `~cleanlab.file.function_or_class_name` ``. + + This uses the [Sphinx's](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-default_role) `default_role = "py:obj"` setting, so the leading tilde shortens the link to only display `function_or_class_name`. + - If you want to additionally specify the module which the function belongs to: + - `` :py:func:`file.function_name ` `` for functions + - ``:py:class:`file.class_name ` `` for classes + + Here you have more control over the text that is displayed to display the module name. When referring to a function that is alternatively defined in other modules as well, always use this option to be more explicit about which module you are referencing. +- Link a tutorial (rst file) from within a source code docstring or rst file: ``:ref:`tutorial_name ` `` +- Link a tutorial notebook (ipynb file) from within a source code docstring or rst file: `` `notebook_name `_ `` . (If the notebook is not the in the same folder as the source code, use a relative path) +- Link a function from within a tutorial notebook: `[function_name](../cleanlab/file.html#cleanlab.file.function_name)` + + Links from master branch tutorials will reference master branch functions, similarly links from tutorials in stable branch will reference stable branch functions since we are using relative paths. - Link a specific section of a notebook from within the notebook: `[section title](#section-title)` - Link a different tutorial notebook from within a tutorial notebook: `[another notebook](another_notebook.html)`. (Note this only works when the other notebook is in same folder as this notebook, otherwise may need to try relative path) - Link another specific section of different notebook from within a tutorial notebook: `[another notebook section title](another_notebook.html#another-notebook-section-title)` diff --git a/README.md b/README.md index 429c8f5393..2b44160e0a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ -![](https://raw.githubusercontent.com/cleanlab/assets/master/cleanlab/cleanlab_logo_open_source_transparent_optimized_size.png) +

+ +

+ -cleanlab automatically finds and fixes errors in any ML dataset. This data-centric AI package facilitates **machine learning with messy, real-world data** by providing **clean lab**els during training. +cleanlab helps you **clean** data and **lab**els by automatically detecting issues in a ML dataset. To facilitate **machine learning with messy, real-world data**, this data-centric AI package uses your *existing* models to estimate dataset problems that can be fixed to train even *better* models. ```python -# cleanlab works with **any classifier**. Yup, you can use sklearn/PyTorch/TensorFlow/XGBoost/etc. +# cleanlab works with **any classifier**. Yup, you can use PyTorch/TensorFlow/OpenAI/XGBoost/etc. cl = cleanlab.classification.CleanLearning(sklearn.YourFavoriteClassifier()) # cleanlab finds data and label issues in **any dataset**... in ONE line of code! @@ -16,13 +19,14 @@ cl.fit(data, labels) # cleanlab estimates the predictions you would have gotten if you had trained with *no* label issues. cl.predict(test_data) -# A true data-centric AI package, cleanlab quantifies class-level issues and overall data quality, for any dataset. +# A universal data-centric AI tool, cleanlab quantifies class-level issues and overall data quality, for any dataset. cleanlab.dataset.health_summary(labels, confident_joint=cl.confident_joint) ``` Get started with: [documentation](https://docs.cleanlab.ai/), [tutorials](https://docs.cleanlab.ai/stable/tutorials/image.html), [examples](https://github.com/cleanlab/examples), and [blogs](https://cleanlab.ai/blog/). - - Learn to run cleanlab on your data in 5 minutes for classification with: [image](https://docs.cleanlab.ai/stable/tutorials/image.html), [text](https://docs.cleanlab.ai/stable/tutorials/text.html), [audio](https://docs.cleanlab.ai/stable/tutorials/audio.html), or [tabular](https://docs.cleanlab.ai/stable/tutorials/tabular.html) data. + - Learn to run cleanlab on your data in 5 minutes for classification with: [image](https://docs.cleanlab.ai/stable/tutorials/datalab/image.html), [text](https://docs.cleanlab.ai/stable/tutorials/datalab/text.html), [audio](https://docs.cleanlab.ai/stable/tutorials/datalab/audio.html), or [tabular](https://docs.cleanlab.ai/stable/tutorials/datalab/tabular.html) data. +- Use cleanlab to automatically: [detect data issues (outliers, duplicates, label errors, etc)](https://docs.cleanlab.ai/stable/tutorials/datalab/datalab_quickstart.html), [train robust models](https://docs.cleanlab.ai/stable/tutorials/indepth_overview.html), [infer consensus + annotator-quality for multi-annotator data](https://docs.cleanlab.ai/stable/tutorials/multiannotator.html), [suggest data to (re)label next (active learning)](https://github.com/cleanlab/examples/blob/master/active_learning_multiannotator/active_learning.ipynb). [![pypi](https://img.shields.io/pypi/v/cleanlab.svg)](https://pypi.org/pypi/cleanlab/) @@ -33,63 +37,27 @@ Get started with: [documentation](https://docs.cleanlab.ai/), [tutorials](https: [![docs](https://img.shields.io/static/v1?logo=github&style=flat&color=pink&label=docs&message=cleanlab)](https://docs.cleanlab.ai/) [![Slack Community](https://img.shields.io/static/v1?logo=slack&style=flat&color=white&label=slack&message=community)](https://cleanlab.ai/slack) [![Twitter](https://img.shields.io/twitter/follow/CleanlabAI?style=social)](https://twitter.com/CleanlabAI) -[![Cleanlab Studio](https://raw.githubusercontent.com/cleanlab/assets/master/shields/cl-studio-shield.svg)](https://cleanlab.ai/studio) - ------ - -
News! (2022) -- cleanlab made accessible for everybody, not just ML researchers (click to learn more) -

-

    -
  • Nov 2022 📖 cleanlab 2.2.0 released! Added better algorithms for: label issues in multi-label classification, data with some classes absent, and estimating the number of label errors in a dataset.
  • -
  • Sep 2022 📖 cleanlab 2.1.0 released! Added support for: data labeled by multiple annotators in cleanlab.multiannotator, token classification with text data in cleanlab.token_classification, out-of-distribution detection in cleanlab.outlier, and CleanLearning with non-numpy-array data (e.g. pandas dataframes, tensorflow/pytorch datasets, etc) in cleanlab.classification.CleanLearning.
  • -
  • April 2022 📖 cleanlab 2.0.0 released! Lays foundations for this library to grow into a general-purpose data-centric AI toolkit.
  • -
  • March 2022 📖 Documentation migrated to new website: docs.cleanlab.ai with quickstart tutorials for image/text/audio/tabular data.
  • -
  • Feb 2022 💻 APIs simplified to make cleanlab accessible for everybody, not just ML researchers
  • -
  • Long-time cleanlab user? Here's how to migrate to cleanlab versions >= 2.0.0.
  • -
-

-
+[![Cleanlab Studio](https://raw.githubusercontent.com/cleanlab/assets/master/shields/cl-studio-shield.svg)](https://cleanlab.ai/studio/?utm_source=github&utm_medium=readme&utm_campaign=clostostudio) -
News! (2021) -- cleanlab finds pervasive label errors in the most common ML datasets (click to learn more) -

-

-

-
-
News! (2020) -- cleanlab supports all OS, achieves state-of-the-art performance (click to learn more) -

-

    -
  • Dec 2020 🎉 cleanlab supports NeurIPS workshop paper (Northcutt, Athalye, & Lin, 2020).
  • -
  • Dec 2020 🤖 cleanlab supports PU learning.
  • -
  • Feb 2020 🤖 cleanlab now natively supports Mac, Linux, and Windows.
  • -
  • Feb 2020 🤖 cleanlab now supports Co-Teaching (Han et al., 2018).
  • -
  • Jan 2020 🎉 cleanlab achieves state-of-the-art on CIFAR-10 with noisy labels. Code to reproduce: examples/cifar10. This is a great place to see how to use cleanlab on real datasets (with predicted probabilities from trained model already precomputed for you).
  • -
+

+ +

+

+ Examples of various issues in Cat/Dog dataset automatically detected by cleanlab (with 1 line of code).

-
- -Release notes for past versions are [here](https://github.com/cleanlab/cleanlab/releases). -Details behind updates are explained in our [blog](https://cleanlab.ai/blog/) and [research papers](https://cleanlab.ai/research/). ## So fresh, so cleanlab -cleanlab **clean**s your data's **lab**els via state-of-the-art *confident learning* algorithms, published in this [paper](https://jair.org/index.php/jair/article/view/12125) and [blog](https://l7.curtisnorthcutt.com/confident-learning). See some of the datasets cleaned with cleanlab at [labelerrors.com](https://labelerrors.com). This package helps you find data and label issues so you can train reliable ML models. +cleanlab **clean**s your data's **lab**els via state-of-the-art *confident learning* algorithms, published in this [paper](https://jair.org/index.php/jair/article/view/12125) and [blog](https://l7.curtisnorthcutt.com/confident-learning). See some of the datasets cleaned with cleanlab at [labelerrors.com](https://labelerrors.com). This data-centric AI tool helps you find data and label issues, so you can train reliable ML models. cleanlab is: -1. **backed by theory** - - with [provable guarantees](https://arxiv.org/abs/1911.00068) of exact estimation of noise and label errors, even with imperfect models. -2. **fast** - - Code is parallelized (< 1 second to find label issues in ImageNet with pre-computed predictions). -4. **easy-to-use** - - Find label issues or train noise-robust models in one line of code (no hyperparameters by default). -6. **general** - - Works with **[any dataset](https://labelerrors.com/)** and **any model**, e.g., TensorFlow, PyTorch, sklearn, XGBoost, Huggingface, etc. +1. **backed by theory** -- with [provable guarantees](https://arxiv.org/abs/1911.00068) of exact label noise estimation, even with imperfect models. +2. **fast** -- code is parallelized and scalable. +4. **easy to use** -- one line of code to find mislabeled data, bad annotators, outliers, or train noise-robust models. +6. **general** -- works with **[any dataset](https://labelerrors.com/)** (text, image, tabular, audio,...) + **any model** (PyTorch, OpenAI, XGBoost,...)
![](https://raw.githubusercontent.com/cleanlab/assets/master/cleanlab/label-errors-examples.png) @@ -103,11 +71,23 @@ cleanlab supports Linux, macOS, and Windows and runs on Python 3.7+. - Get started [here](https://docs.cleanlab.ai/)! Install via `pip` or `conda` as described [here](https://docs.cleanlab.ai/). - Developers who install the bleeding-edge from source should refer to [this master branch documentation](https://docs.cleanlab.ai/master/index.html). +- For help, check out our detailed [FAQ](https://docs.cleanlab.ai/stable/tutorials/faq.html), [Github Issues](https://github.com/cleanlab/cleanlab/issues?q=is%3Aissue), or [Slack](https://cleanlab.ai/slack). We welcome any questions! + +**Practicing data-centric AI can look like this:** +1. Train initial ML model on original dataset. +2. Utilize this model to diagnose data issues (via cleanlab methods) and improve the dataset. +3. Train the same model on the improved dataset. +4. Try various modeling techniques to further improve performance. + +Most folks jump from Step 1 → 4, but you may achieve big gains without *any* change to your modeling code by using cleanlab! +Continuously boost performance by iterating Steps 2 → 4 (and try to evaluate with *cleaned* data). + +![](https://raw.githubusercontent.com/cleanlab/assets/master/cleanlab/dcai_flowchart.png) ## Use cleanlab with any model for most ML tasks -All features of cleanlab work with **any dataset** and **any model**. Yes, any model: scikit-learn, PyTorch, Tensorflow, Keras, JAX, HuggingFace, MXNet, XGBoost, etc. +All features of cleanlab work with **any dataset** and **any model**. Yes, any model: PyTorch, Tensorflow, Keras, JAX, HuggingFace, OpenAI, XGBoost, scikit-learn, etc. If you use a sklearn-compatible classifier, all cleanlab methods work out-of-the-box.
@@ -117,7 +97,7 @@ It’s also easy to use your favorite non-sklearn-compatible model (click to cleanlab can find label issues from any model's predicted class probabilities if you can produce them yourself. -Some other cleanlab functionality requires your model to be sklearn-compatible. +Some cleanlab functionality may require your model to be sklearn-compatible. There's nothing you need to do if your model already has `.fit()`, `.predict()`, and `.predict_proba()` methods. Otherwise, just wrap your custom model into a Python class that inherits the `sklearn.base.BaseEstimator`: @@ -150,300 +130,23 @@ cl.predict(test_data) More details are provided in documentation of [cleanlab.classification.CleanLearning](https://docs.cleanlab.ai/stable/cleanlab/classification.html). -Note, some libraries exist to give you sklearn-compatibility for free. For PyTorch, check out the [skorch](https://skorch.readthedocs.io/) Python library which will wrap your PyTorch model into a sklearn-compatible model ([example](https://docs.cleanlab.ai/stable/tutorials/image.html)). For TensorFlow/Keras, check out [SciKeras](https://www.adriangb.com/scikeras/) ([example](https://docs.cleanlab.ai/stable/tutorials/text.html)) or [our own Keras wrapper](https://docs.cleanlab.ai/stable/cleanlab/experimental/keras.html). Many libraries also already offer a special scikit-learn API, for example: [XGBoost](https://xgboost.readthedocs.io/en/stable/python/python_api.html#module-xgboost.sklearn) or [LightGBM](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.LGBMClassifier.html). +Note, some libraries exist to give you sklearn-compatibility for free. For PyTorch, check out the [skorch](https://skorch.readthedocs.io/) Python library which will wrap your PyTorch model into a sklearn-compatible model ([example](https://docs.cleanlab.ai/stable/tutorials/image.html)). For TensorFlow/Keras, check out our [Keras wrapper](https://docs.cleanlab.ai/stable/cleanlab/models/keras.html). Many libraries also already offer a special scikit-learn API, for example: [XGBoost](https://xgboost.readthedocs.io/en/stable/python/python_api.html#module-xgboost.sklearn) or [LightGBM](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.LGBMClassifier.html).
-cleanlab is useful across a wide variety of Machine Learning tasks. Specific tasks this package offers dedicated functionality for include: +cleanlab is useful across a wide variety of Machine Learning tasks. Specific tasks this data-centric AI solution offers dedicated functionality for include: 1. [Binary and multi-class classification](https://docs.cleanlab.ai/stable/tutorials/indepth_overview.html) 2. [Multi-label classification](https://docs.cleanlab.ai/stable/tutorials/multilabel_classification.html) (e.g. image/document tagging) 3. [Token classification](https://docs.cleanlab.ai/stable/tutorials/token_classification.html) (e.g. entity recognition in text) 4. [Classification with data labeled by multiple annotators](https://docs.cleanlab.ai/stable/tutorials/multiannotator.html) -5. [Out of distribution detection](https://docs.cleanlab.ai/stable/tutorials/outliers.html) +5. [Active learning with multiple annotators](https://github.com/cleanlab/examples/blob/master/active_learning_multiannotator/active_learning.ipynb) (suggest which data to label or re-label to improve model most) +6. [Outlier and out of distribution detection](https://docs.cleanlab.ai/stable/tutorials/outliers.html) For many other ML tasks, cleanlab can still help you improve your dataset if appropriately applied. +Many practical applications are demonstrated in our [Example Notebooks](https://github.com/cleanlab/examples). -## Cool cleanlab applications - -
-Reproducing results in Confident Learning paper -(click to learn more) - -
- -For additional details, check out the: [confidentlearning-reproduce repository](https://github.com/cgnorthcutt/confidentlearning-reproduce). - -### State of the Art Learning with Noisy Labels in CIFAR - -A step-by-step guide to reproduce these results is available [here](https://github.com/cleanlab/examples/tree/master/contrib/v1/cifar10). This guide is also a good tutorial for using cleanlab on any large dataset. You'll need to `git clone` -[confidentlearning-reproduce](https://github.com/cgnorthcutt/confidentlearning-reproduce) which contains the data and files needed to reproduce the CIFAR-10 results. - -![](https://raw.githubusercontent.com/cleanlab/assets/master/cleanlab/cifar10_benchmarks.png) - -Comparison of confident learning (CL), as implemented in cleanlab, versus seven recent methods for learning with noisy labels in CIFAR-10. Highlighted cells show CL robustness to sparsity. The five CL methods estimate label issues, remove them, then train on the cleaned data using [Co-Teaching](https://github.com/cleanlab/cleanlab/blob/master/cleanlab/experimental/coteaching.py). - -Observe how cleanlab (i.e. the CL method) is robust to large sparsity in label noise whereas prior art tends to reduce in performance for increased sparsity, as shown by the red highlighted regions. This is important because real-world label noise is often sparse, e.g. a tiger is likely to be mislabeled as a lion, but not as most other classes like airplane, bathtub, and microwave. - -### Find label issues in ImageNet - -Use cleanlab to identify \~100,000 label errors in the 2012 ILSVRC ImageNet training dataset: [examples/imagenet](https://github.com/cleanlab/examples/tree/master/contrib/v1/imagenet). - -![](https://raw.githubusercontent.com/cleanlab/assets/master/cleanlab/imagenet_train_label_errors_32.jpg) - -Label issues in ImageNet train set found via cleanlab. Label Errors are boxed in red. Ontological issues in green. Multi-label images in blue. - -### Find Label Errors in MNIST - -Use cleanlab to identify \~50 label errors in the MNIST dataset: [examples/mnist](https://github.com/cleanlab/examples/tree/master/contrib/v1/mnist). - -![](https://raw.githubusercontent.com/cleanlab/assets/master/cleanlab/mnist_training_label_errors24_prune_by_noise_rate.png) - -Top 24 least-confident labels in the original MNIST **train** dataset, algorithmically identified via cleanlab. Examples are ordered left-right, top-down by increasing self-confidence (predicted probability that the **given** label is correct), denoted **conf** in teal. The most-likely correct label (with largest predicted probability) is in green. Overt label errors highlighted in red. - -
-
- -
-Learning with noisy labels across 4 data distributions and 9 classifiers -(click to learn more) - -
- -cleanlab is a general tool that can learn with noisy labels regardless of dataset distribution or classifier type: [examples/classifier\_comparison](https://github.com/cleanlab/examples/blob/master/classifier_comparison/classifier_comparison.ipynb). - -![](https://raw.githubusercontent.com/cleanlab/assets/master/cleanlab/demo_cleanlab_across_datasets_and_classifiers.png) - -Each sub-figure above depicts the decision boundary learned using [cleanlab.classification.CleanLearning](https://github.com/cleanlab/cleanlab/blob/master/cleanlab/classification.py#L141) in the presence of extreme (\~35%) label errors (circled in green). Label noise is class-conditional (not uniformly random). Columns are organized by the classifier used, except the left-most column which depicts the ground-truth data distribution. Rows are organized by dataset. - -Each sub-figure depicts accuracy scores on a test set (with correct non-noisy labels) as decimal values: - -* LEFT (in black): The classifier test accuracy trained with perfect labels (no label errors). -* MIDDLE (in blue): The classifier test accuracy trained with noisy labels using cleanlab. -* RIGHT (in white): The baseline classifier test accuracy trained with noisy labels. - -As an example, the table below is the noise matrix (noisy channel) *P(s | y) -characterizing the label noise for the first dataset row in the figure. *s* represents the observed noisy labels and *y* represents the latent, true labels. The trace of this matrix is 2.6. A trace of 4 implies no label noise. A cell in this matrix is read like: "Around 38% of true underlying '3' labels were randomly flipped to '2' labels in the -observed dataset." - -| `p(label︱y)` | y=0 | y=1 | y=2 | y=3 | -|--------------|------|------|------|------| -| label=0 | 0.55 | 0.01 | 0.07 | 0.06 | -| label=1 | 0.22 | 0.87 | 0.24 | 0.02 | -| label=2 | 0.12 | 0.04 | 0.64 | 0.38 | -| label=3 | 0.11 | 0.08 | 0.05 | 0.54 | - -
-
- -
-ML research using cleanlab -(click to learn more) - -
- -Researchers may find some components of this package useful for evaluating algorithms for ML with noisy labels. For additional details/notation, refer to [the Confident Learning paper](https://jair.org/index.php/jair/article/view/12125). - -### Methods to Standardize Research with Noisy Labels - -cleanlab supports a number of functions to generate noise for benchmarking and standardization in research. This next example shows how to generate valid, class-conditional, uniformly random noisy channel matrices: - -``` python -# Generate a valid (necessary conditions for learnability are met) noise matrix for any trace > 1 -from cleanlab.benchmarking.noise_generation import generate_noise_matrix_from_trace -noise_matrix=generate_noise_matrix_from_trace( - K=number_of_classes, - trace=float_value_greater_than_1_and_leq_K, - py=prior_of_y_actual_labels_which_is_just_an_array_of_length_K, - frac_zero_noise_rates=float_from_0_to_1_controlling_sparsity, -) - -# Check if a noise matrix is valid (necessary conditions for learnability are met) -from cleanlab.benchmarking.noise_generation import noise_matrix_is_valid -is_valid=noise_matrix_is_valid( - noise_matrix, - prior_of_y_which_is_just_an_array_of_length_K, -) -``` - -For a given noise matrix, this example shows how to generate noisy labels. Methods can be seeded for reproducibility. - -``` python -# Generate noisy labels using the noise_marix. Guarantees exact amount of noise in labels. -from cleanlab.benchmarking.noise_generation import generate_noisy_labels -s_noisy_labels = generate_noisy_labels(y_hidden_actual_labels, noise_matrix) - -# This package is a full of other useful methods for learning with noisy labels. -# The tutorial stops here, but you don't have to. Inspect method docstrings for full docs. -``` - -
-
- -
-cleanlab for advanced users -(click to learn more) - -
- -Many methods and their default parameters are not covered here. Check out the [documentation for the master branch version](https://docs.cleanlab.ai/master/) for the full suite of features supported by the cleanlab API. - -## Use any custom model's predicted probabilities to find label errors in 1 line of code - -pred_probs (num_examples x num_classes matrix of predicted probabilities) should already be computed on your own, with any classifier. For best results, pred_probs should be obtained in a holdout/out-of-sample manner (e.g. via cross-validation). -* cleanlab can do this for you via [`cleanlab.count.estimate_cv_predicted_probabilities`](https://docs.cleanlab.ai/master/cleanlab/count.html)] -* Tutorial with more info: [[here](https://docs.cleanlab.ai/stable/tutorials/pred_probs_cross_val.html)] -* Examples how to compute pred_probs with: [[CNN image classifier (PyTorch)](https://docs.cleanlab.ai/stable/tutorials/image.html)], [[NN text classifier (TensorFlow)](https://docs.cleanlab.ai/stable/tutorials/text.html)] - -```python -# label issues are ordered by likelihood of being an error. First index is most likely error. -from cleanlab.filter import find_label_issues - -ordered_label_issues = find_label_issues( # One line of code! - labels=numpy_array_of_noisy_labels, - pred_probs=numpy_array_of_predicted_probabilities, - return_indices_ranked_by='normalized_margin', # Orders label issues - ) -``` - -Pre-computed **out-of-sample** predicted probabilities for CIFAR-10 train set are available: [here](https://github.com/cleanlab/examples/tree/master/contrib/v1/cifar10#pre-computed-psx-for-every-noise--sparsity-condition). - -## Fully characterize label noise and uncertainty in your dataset. - -*s* denotes a random variable that represents the observed, noisy label and *y* denotes a random variable representing the hidden, actual labels. Both *s* and *y* take any of the m classes as values. The cleanlab package supports different levels of granularity for computation depending on the needs of the user. Because of this, we support multiple alternatives, all no more than a few lines, to estimate these latent distribution arrays, enabling the user to reduce computation time by only computing what they need to compute, as seen in the examples below. - -Throughout these examples, you’ll see a variable called *confident\_joint*. The confident joint is an m x m matrix (m is the number of classes) that counts, for every observed, noisy class, the number of examples that confidently belong to every latent, hidden class. It counts the number of examples that we are confident are labeled correctly or incorrectly for every pair of observed and unobserved classes. The confident joint is an unnormalized estimate of the complete-information latent joint distribution, *Ps,y*. - -The label flipping rates are denoted *P(s | y)*, the inverse rates are *P(y | s)*, and the latent prior of the unobserved, true labels, *p(y)*. - -Most of the methods in the **cleanlab** package start by first estimating the *confident\_joint*. You can learn more about this in the [confident learning paper](https://arxiv.org/abs/1911.00068). - -### Option 1: Compute the confident joint and predicted probs first. Stop if that’s all you need. - -``` python -from cleanlab.count import estimate_latent -from cleanlab.count import estimate_confident_joint_and_cv_pred_proba - -# Compute the confident joint and the n x m predicted probabilities matrix (pred_probs), -# for n examples, m classes. Stop here if all you need is the confident joint. -confident_joint, pred_probs = estimate_confident_joint_and_cv_pred_proba( - X=X_train, - labels=train_labels_with_errors, - clf=logreg(), # default, you can use any classifier -) - -# Estimate latent distributions: p(y) as est_py, P(s|y) as est_nm, and P(y|s) as est_inv -est_py, est_nm, est_inv = estimate_latent( - confident_joint, - labels=train_labels_with_errors, -) -``` - -### Option 2: Estimate the latent distribution matrices in a single line of code. - -``` python -from cleanlab.count import estimate_py_noise_matrices_and_cv_pred_proba -est_py, est_nm, est_inv, confident_joint, pred_probs = estimate_py_noise_matrices_and_cv_pred_proba( - X=X_train, - labels=train_labels_with_errors, -) -``` - -### Option 3: Skip computing the predicted probabilities if you already have them. - -``` python -# Already have pred_probs? (n x m matrix of predicted probabilities) -# For example, you might get them from a pre-trained model (like resnet on ImageNet) -# With the cleanlab package, you estimate directly with pred_probs. -from cleanlab.count import estimate_py_and_noise_matrices_from_probabilities -est_py, est_nm, est_inv, confident_joint = estimate_py_and_noise_matrices_from_probabilities( - labels=train_labels_with_errors, - pred_probs=pred_probs, -) -``` - -## Completely characterize label noise in a dataset: - -The joint probability distribution of noisy and true labels, *P(s,y)*, completely characterizes label noise with a class-conditional *m x m* matrix. - -``` python -from cleanlab.count import estimate_joint -joint = estimate_joint( - labels=noisy_labels, - pred_probs=probabilities, - confident_joint=None, # Provide if you have it already -) -``` - -
-
- -
-Positive-Unlabeled Learning -(click to learn more) - -
- -Positive-Unlabeled (PU) learning (in which your data only contains a few positively labeled examples with the rest unlabeled) is just a special case of [CleanLearning](https://github.com/cleanlab/cleanlab/blob/master/cleanlab/classification.py#L141) when one of the classes has no error. `P` stands for the positive class and **is assumed to have zero label errors** and `U` stands for unlabeled data, but in practice, we just assume the `U` class is a noisy negative class that actually contains some positive examples. Thus, the goal of PU learning is to (1) estimate the proportion of negatively labeled examples that actually belong to the positive class (see`fraction\_noise\_in\_unlabeled\_class` in the last example), (2) find the errors (see last example), and (3) train on clean data (see first example below). cleanlab does all three, taking into account that there are no label errors in whichever class you specify as positive. - -There are two ways to use cleanlab for PU learning. We'll look at each here. - -Method 1. If you are using the cleanlab classifier [CleanLearning()](https://github.com/cleanlab/cleanlab/blob/master/cleanlab/classification.py#L141), and your dataset has exactly two classes (positive = 1, and negative = 0), PU -learning is supported directly in cleanlab. You can perform PU learning like this: - -``` python -from cleanlab.classification import CleanLearning -from sklearn.linear_model import LogisticRegression -# Wrap around any classifier. Yup, you can use sklearn/pyTorch/TensorFlow/FastText/etc. -pu_class = 0 # Should be 0 or 1. Label of class with NO ERRORS. (e.g., P class in PU) -cl = CleanLearning(clf=LogisticRegression(), pulearning=pu_class) -cl.fit(X=X_train_data, labels=train_noisy_labels) -# Estimate the predictions you would have gotten by training with *no* label errors. -predicted_test_labels = cl.predict(X_test) -``` - -Method 2. However, you might be using a more complicated classifier that doesn't work well with [CleanLearning](https://github.com/cleanlab/cleanlab/blob/master/cleanlab/classification.py#L141) (see this example for CIFAR-10). Or you might have 3 or more classes. Here's how to use cleanlab for PU learning in this situation. To let cleanlab know which class has no error (in standard PU learning, this is the P class), you need to set the threshold for that class to 1 (1 means the probability that the labels of that class are correct is 1, i.e. that class has no -error). Here's the code: - -``` python -import numpy as np -# K is the number of classes in your dataset -# pred_probs are the cross-validated predicted probabilities. -# s is the array/list/iterable of noisy labels -# pu_class is a 0-based integer for the class that has no label errors. -thresholds = np.asarray([np.mean(pred_probs[:, k][s == k]) for k in range(K)]) -thresholds[pu_class] = 1.0 -``` - -Now you can use cleanlab however you were before. Just be sure to pass in `thresholds` as a parameter wherever it applies. For example: - -``` python -# Uncertainty quantification (characterize the label noise -# by estimating the joint distribution of noisy and true labels) -cj = compute_confident_joint(s, pred_probs, thresholds=thresholds, ) -# Now the noise (cj) has been estimated taking into account that some class(es) have no error. -# We can use cj to find label errors like this: -indices_of_label_issues = find_label_issues(s, pred_probs, confident_joint=cj, ) - -# In addition to label issues, cleanlab can find the fraction of noise in the unlabeled class. -# First we need the inv_noise_matrix which contains P(y|s) (proportion of mislabeling). -_, _, inv_noise_matrix = estimate_latent(confident_joint=cj, labels=s, ) -# Because inv_noise_matrix contains P(y|s), p (y = anything | labels = pu_class) should be 0 -# because the prob(true label is something else | example is in pu_class) is 0. -# What's more interesting is p(y = anything | s is not put_class), or in the binary case -# this translates to p(y = pu_class | s = 1 - pu_class) because pu_class is 0 or 1. -# So, to find the fraction_noise_in_unlabeled_class, for binary, you just compute: -fraction_noise_in_unlabeled_class = inv_noise_matrix[pu_class][1 - pu_class] -``` - -Now that you have `indices_of_label_errors`, you can remove those label issues and train on clean data (or only remove some of the label issues and iteratively use confident learning / cleanlab to improve results). - -
-
- -Many other practical applications are demonstrated in our [Example Notebooks](https://github.com/cleanlab/examples) - ## Citation and related publications cleanlab is based on peer-reviewed research. Here are relevant papers to cite if you use this package: @@ -513,7 +216,7 @@ cleanlab is based on peer-reviewed research. Here are relevant papers to cite if
CROWDLAB for data with multiple annotators (NeurIPS '22) (click to show bibtex) @inproceedings{goh2022crowdlab, - title={Utilizing supervised models to infer consensus labels and their quality from data with multiple annotators}, + title={CROWDLAB: Supervised learning to infer consensus labels and quality scores for data with multiple annotators}, author={Goh, Hui Wen and Tkachenko, Ulyana and Mueller, Jonas}, booktitle={NeurIPS Human in the Loop Learning Workshop}, year={2022} @@ -521,21 +224,75 @@ cleanlab is based on peer-reviewed research. Here are relevant papers to cite if
+
ActiveLab: Active learning with data re-labeling (ICLR '23) (click to show bibtex) + + @inproceedings{goh2023activelab, + title={ActiveLab: Active Learning with Re-Labeling by Multiple Annotators}, + author={Goh, Hui Wen and Mueller, Jonas}, + booktitle={ICLR Workshop on Trustworthy ML}, + year={2023} + } + +
+ +
Incorrect Annotations in Multi-Label Classification (ICLR '23) (click to show bibtex) + + @inproceedings{thyagarajan2023multilabel, + title={Identifying Incorrect Annotations in Multi-Label Classification Data}, + author={Thyagarajan, Aditya and Snorrason, Elías and Northcutt, Curtis and Mueller, Jonas}, + booktitle={ICLR Workshop on Trustworthy ML}, + year={2023} + } + +
+ +
Detecting Dataset Drift and Non-IID Sampling (ICML '23) (click to show bibtex) + + @inproceedings{cummings2023drift, + title={Detecting Dataset Drift and Non-IID Sampling via k-Nearest Neighbors}, + author={Cummings, Jesse and Snorrason, Elías and Mueller, Jonas}, + booktitle={ICML Workshop on Data-centric Machine Learning Research}, + year={2023} + } + +
+ +
Detecting Errors in Numerical Data (ICML '23) (click to show bibtex) + + @inproceedings{zhou2023errors, + title={Detecting Errors in Numerical Data via any Regression Model}, + author={Zhou, Hang and Mueller, Jonas and Kumar, Mayank and Wang, Jane-Ling and Lei, Jing}, + booktitle={ICML Workshop on Data-centric Machine Learning Research}, + year={2023} + } + +
+ To understand/cite other cleanlab functionality not described above, check out our [additional publications](https://cleanlab.ai/research/). ## Other resources +- [Example Notebooks demonstrating practical applications of this package](https://github.com/cleanlab/examples) + - [Cleanlab Blog](https://cleanlab.ai/blog/) - [Blog post: Introduction to Confident Learning](https://l7.curtisnorthcutt.com/confident-learning) - [NeurIPS 2021 paper: Pervasive Label Errors in Test Sets Destabilize Machine Learning Benchmarks](https://arxiv.org/abs/2103.14749) -- [Cleanlab Studio](https://cleanlab.ai/studio): No-code Data Improvement +- [Introduction to Data-centric AI (MIT IAP Course 2023)](https://dcai.csail.mit.edu/) + +- [Release notes for past versions](https://github.com/cleanlab/cleanlab/releases) -While this open-source library **finds** data issues, an interface is needed to efficiently **fix** these issues in your dataset. [Cleanlab Studio](https://cleanlab.ai/studio) is a no-code platform to find and fix problems in real-world ML datasets. Studio automatically runs optimized versions of the algorithms from this open-source library on top of AutoML models fit to your data, and presents detected issues in a smart data editing interface. Think of it like a data cleaning assistant that helps you quickly improve the quality of your data (via AI/automation + streamlined UX). +- [Cleanlab Studio](https://cleanlab.ai/studio/?utm_source=github&utm_medium=readme&utm_campaign=clostostudio): *No-code Data Improvement* + +While this open-source library **finds** data issues, its utility depends on you having a decent existing ML model and an interface to efficiently **fix** these issues in your dataset. Providing all these pieces, [Cleanlab Studio](https://cleanlab.ai/studio/?utm_source=github&utm_medium=readme&utm_campaign=clostostudio) is a no-code platform to **find and fix** problems in real-world ML datasets. Studio automatically runs optimized versions of the algorithms from this open-source library on top of AutoML & Foundation models fit to your data, and presents detected issues in a smart data editing interface. It's a data cleaning assistant to quickly turn unreliable data into reliable models/insights (via AI/automation + streamlined UX). [Try it for free!](https://cleanlab.typeform.com/to/NLnU1XZF?typeform-source=cleanlab.ai) + +

+ +

## Join our community @@ -543,21 +300,26 @@ While this open-source library **finds** data issues, an interface is needed to * Have ideas for the future of cleanlab? How are you using cleanlab? [Join the discussion](https://github.com/cleanlab/cleanlab/discussions) and check out [our active/planned Projects and what we could use your help with](https://github.com/cleanlab/cleanlab/projects). -* Interested in contributing? See the [contributing guide](CONTRIBUTING.md) and [ideas on useful contributions](https://github.com/cleanlab/cleanlab/wiki#ideas-for-contributing-to-cleanlab). We welcome your help building a standard open-source library for data-centric AI! +* Interested in contributing? See the [contributing guide](CONTRIBUTING.md) and [ideas on useful contributions](https://github.com/cleanlab/cleanlab/wiki#ideas-for-contributing-to-cleanlab). We welcome your help building a standard open-source platform for data-centric AI! * Have code improvements for cleanlab? See the [development guide](DEVELOPMENT.md). -* Have an issue with cleanlab? [Search existing issues](https://github.com/cleanlab/cleanlab/issues?q=is%3Aissue) or [submit a new issue](https://github.com/cleanlab/cleanlab/issues/new). +* Have an issue with cleanlab? Search [our FAQ](https://docs.cleanlab.ai/stable/tutorials/faq.html) and [existing issues](https://github.com/cleanlab/cleanlab/issues?q=is%3Aissue), or [submit a new issue](https://github.com/cleanlab/cleanlab/issues/new). * Need professional help with cleanlab? -Join our [\#help Slack channel](https://cleanlab.ai/slack) and message one of our core developers, Jonas Mueller, or schedule a meeting via email: team@cleanlab.ai +Join our [\#help Slack channel](https://cleanlab.ai/slack) and message us there, or reach out via email: team@cleanlab.ai ## License -Copyright (c) 2017-2022 Cleanlab Inc. +Copyright (c) 2017 Cleanlab Inc. cleanlab is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. cleanlab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See [GNU Affero General Public LICENSE](https://github.com/cleanlab/cleanlab/blob/master/LICENSE) for details. +You can email us to discuss licensing: team@cleanlab.ai + +### Commercial licensing + +Commercial licensing is available for teams and enterprises that want to use cleanlab in production workflows, but are unable to open-source their code. Please contact us [here](mailto:sales@cleanlab.ai). diff --git a/cleanlab/__init__.py b/cleanlab/__init__.py index 5746a49a21..7ec7a55e50 100644 --- a/cleanlab/__init__.py +++ b/cleanlab/__init__.py @@ -9,3 +9,49 @@ from . import outlier from . import token_classification from . import multilabel_classification +from . import object_detection +from . import regression +from . import segmentation + + +class DatalabUnavailable: + def __init__(self, message): + self.message = message + + def __getattr__(self, name): + message = self.message + f" (raised when trying to access {name})" + raise ImportError(message) + + def __call__(self, *args, **kwargs): + message = ( + self.message + f" (raised when trying to call with args: {args}, kwargs: {kwargs})" + ) + raise ImportError(message) + + +def _datalab_import_factory(): + try: + from .datalab.datalab import Datalab as _Datalab + + return _Datalab + except ImportError: + return DatalabUnavailable( + "Datalab is not available due to missing dependencies. " + "To install Datalab, run `pip install 'cleanlab[datalab]'`." + ) + + +def _issue_manager_import_factory(): + try: + from .datalab.issue_manager import IssueManager as _IssueManager + + return _IssueManager + except ImportError: + return DatalabUnavailable( + "IssueManager is not available due to missing dependencies for Datalab. " + "To install Datalab, run `pip install 'cleanlab[datalab]'`." + ) + + +Datalab = _datalab_import_factory() +IssueManager = _issue_manager_import_factory() diff --git a/cleanlab/benchmarking/noise_generation.py b/cleanlab/benchmarking/noise_generation.py index 47937c4453..63c1352bbb 100644 --- a/cleanlab/benchmarking/noise_generation.py +++ b/cleanlab/benchmarking/noise_generation.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -26,6 +26,7 @@ import numpy as np from cleanlab.internal.util import value_counts +from cleanlab.internal.constants import FLOATING_POINT_COMPARISON def noise_matrix_is_valid(noise_matrix, py, *, verbose=False) -> bool: @@ -65,7 +66,7 @@ def noise_matrix_is_valid(noise_matrix, py, *, verbose=False) -> bool: joint_noise = np.multiply(noise_matrix, py) # / float(N) # Check that joint_probs is valid probability matrix - if not (abs(joint_noise.sum() - 1.0) < 1e-6): + if not (abs(joint_noise.sum() - 1.0) < FLOATING_POINT_COMPARISON): return False # Check that noise_matrix is a valid matrix @@ -386,11 +387,9 @@ def generate_n_rand_probabilities_that_sum_to_m( An array of probabilities. """ - epsilon = 1e-6 # Imprecision allowed for inequalities with floats - if n == 0: return np.array([]) - if (max_prob + epsilon) < m / float(n): + if (max_prob + FLOATING_POINT_COMPARISON) < m / float(n): raise ValueError( "max_prob must be greater or equal to m / n, but " + "max_prob = " @@ -402,7 +401,7 @@ def generate_n_rand_probabilities_that_sum_to_m( + ", m / n = " + str(m / float(n)) ) - if min_prob > (m + epsilon) / float(n): + if min_prob > (m + FLOATING_POINT_COMPARISON) / float(n): raise ValueError( "min_prob must be less or equal to m / n, but " + "max_prob = " @@ -422,7 +421,7 @@ def generate_n_rand_probabilities_that_sum_to_m( min_val = min(result) max_val = max(result) - while max_val > (max_prob + epsilon): + while max_val > (max_prob + FLOATING_POINT_COMPARISON): new_min = min_val + (max_val - max_prob) # This adjustment prevents the new max from always being max_prob. adjustment = (max_prob - new_min) * np.random.rand() @@ -433,7 +432,7 @@ def generate_n_rand_probabilities_that_sum_to_m( min_val = min(result) max_val = max(result) - while min_val < (min_prob - epsilon): + while min_val < (min_prob - FLOATING_POINT_COMPARISON): min_val = min(result) max_val = max(result) new_max = max_val - (min_prob - min_val) diff --git a/cleanlab/classification.py b/cleanlab/classification.py index 3ce2362bcd..63456aac8f 100644 --- a/cleanlab/classification.py +++ b/cleanlab/classification.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -120,7 +120,10 @@ def score(self, X, y, sample_weight=None): import pandas as pd import inspect import warnings -from typing import TypeVar, Optional +from typing import Optional, TYPE_CHECKING + +if TYPE_CHECKING: # pragma: no cover + from typing_extensions import Self from cleanlab.rank import get_label_quality_scores from cleanlab import filter @@ -147,9 +150,6 @@ def score(self, X, y, sample_weight=None): ) -TCleanLearning = TypeVar("TCleanLearning", bound="CleanLearning") # self type for the class - - class CleanLearning(BaseEstimator): # Inherits sklearn classifier """ CleanLearning = Machine Learning with cleaned data (even when training on messy, error-ridden data). @@ -204,8 +204,9 @@ class CleanLearning(BaseEstimator): # Inherits sklearn classifier find_label_issues_kwargs : dict, optional Keyword arguments to pass into :py:func:`filter.find_label_issues - `. Options that may especially impact - accuracy include: `filter_by`, `frac_noise`, `min_examples_per_class`. + `. Particularly useful options include: + `filter_by`, `frac_noise`, `min_examples_per_class` (which all impact ML accuracy), + `n_jobs` (set this to 1 to disable multi-processing if it's causing issues). label_quality_scores_kwargs : dict, optional Keyword arguments to pass into :py:func:`rank.get_label_quality_scores @@ -229,7 +230,6 @@ def __init__( label_quality_scores_kwargs={}, verbose=False, ): - if clf is None: # Use logistic regression if no classifier is provided. clf = LogReg(multi_class="auto", solver="lbfgs") @@ -266,7 +266,7 @@ def __init__( self.clf_final_kwargs = None def fit( - self: TCleanLearning, + self, X, labels=None, *, @@ -280,7 +280,7 @@ def fit( clf_final_kwargs={}, validation_func=None, y=None, - ) -> TCleanLearning: + ) -> "Self": """ Train the model `clf` with error-prone, noisy labels as if the model had been instead trained on a dataset with the correct labels. @@ -645,7 +645,6 @@ def score(self, X, y, sample_weight=None) -> float: """ if hasattr(self.clf, "score"): - # Check if sample_weight in clf.score() if "sample_weight" in inspect.getfullargspec(self.clf.score).args: return self.clf.score(X, y, sample_weight=sample_weight) @@ -841,6 +840,7 @@ def find_label_issues( pred_probs=pred_probs, thresholds=thresholds, ) + # if pulearning == the integer specifying the class without noise. if self.num_classes == 2 and self.pulearning is not None: # pragma: no cover # pulearning = 1 (no error in 1 class) implies p(label=1|true_label=0) = 0 @@ -853,6 +853,12 @@ def find_label_issues( self.confident_joint[self.pulearning][1 - self.pulearning] = 0 self.confident_joint[1 - self.pulearning][1 - self.pulearning] = 1 + # Add confident joint to find label issue args if it is not previously specified + if "confident_joint" not in self.find_label_issues_kwargs.keys(): + # however does not add if users specify filter_by="confident_learning", as it will throw a warning + if not self.find_label_issues_kwargs.get("filter_by") == "confident_learning": + self.find_label_issues_kwargs["confident_joint"] = self.confident_joint + labels = labels_to_array(labels) if self.verbose: print("Using predicted probabilities to identify label issues ...") @@ -927,9 +933,6 @@ def save_space(self): self.label_issues_mask = None self.find_label_issues_kwargs = None self.label_quality_scores_kwargs = None - self.label_issues_df = None - self.label_issues_mask = None - self.sample_weight = None self.confident_joint = None self.py = None self.ps = None diff --git a/cleanlab/count.py b/cleanlab/count.py index 65db814bfb..d9b22feedb 100644 --- a/cleanlab/count.py +++ b/cleanlab/count.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -38,6 +38,8 @@ from cleanlab.typing import LabelLike from cleanlab.internal.multilabel_utils import stack_complement, get_onehot_num_classes +from cleanlab.internal.constants import TINY_VALUE, CONFIDENT_THRESHOLDS_LOWER_BOUND + from cleanlab.internal.util import ( value_counts_fill_missing_classes, clip_values, @@ -49,7 +51,6 @@ get_unique_classes, is_torch_dataset, is_tensorflow_dataset, - TINY_VALUE, ) from cleanlab.internal.latent_algebra import ( compute_inv_noise_matrix, @@ -87,21 +88,28 @@ def num_label_issues( Array of estimated class label error statisics used for identifying label issues, in same format expected by :py:func:`filter.find_label_issues ` function. The `confident_joint` can be computed using :py:func:`count.compute_confident_joint `. - If not provided, it is internally computed from the given (noisy) `labels` and `pred_probs`. + It is internally computed from the given (noisy) `labels` and `pred_probs`. estimation_method : Method for estimating the number of label issues in dataset by counting the examples in the off-diagonal of the `confident_joint` ``P(label=i, true_label=j)``. - - ``'off_diagonal'``: Counts the number of examples in the off-diagonal of the `confident_joint`. Returns the same value as ``sum(find_label_issues(filter_by='confident_learning'))`` - - ``'off_diagonal_calibrated'``: Calibrates confident joint estimate ``P(label=i, true_label=j)`` such that - ``np.sum(cj) == len(labels)`` and ``np.sum(cj, axis = 1) == np.bincount(labels)`` before counting the number - of examples in the off-diagonal. Number will always be equal to or greater than - ``estimate_issues='off_diagonal'``. You can use this value as the cutoff threshold used with ranking/scoring - functions from :py:mod:`cleanlab.rank` with `num_label_issues` over ``estimation_method='off_diagonal'`` in - two cases: - 1. As we add more label and data quality scoring functions in :py:mod:`cleanlab.rank`, this approach will always work. - 2. If you have a custom score to rank your data by label quality and you just need to know the cut-off of likely label issues. - - TL;DR: use this method to get the most accurate estimate of number of label issues when you don't need the indices of the label issues. + + * ``'off_diagonal'``: Counts the number of examples in the off-diagonal of the `confident_joint`. Returns the same value as ``sum(find_label_issues(filter_by='confident_learning'))`` + + * ``'off_diagonal_calibrated'``: Calibrates confident joint estimate ``P(label=i, true_label=j)`` such that + ``np.sum(cj) == len(labels)`` and ``np.sum(cj, axis = 1) == np.bincount(labels)`` before counting the number + of examples in the off-diagonal. Number will always be equal to or greater than + ``estimate_issues='off_diagonal'``. You can use this value as the cutoff threshold used with ranking/scoring + functions from :py:mod:`cleanlab.rank` with `num_label_issues` over ``estimation_method='off_diagonal'`` in + two cases: + + #. As we add more label and data quality scoring functions in :py:mod:`cleanlab.rank`, this approach will always work. + #. If you have a custom score to rank your data by label quality and you just need to know the cut-off of likely label issues. + + * ``'off_diagonal_custom'``: Counts the number of examples in the off-diagonal of a provided `confident_joint` matrix. + + TL;DR: Use this method to get the most accurate estimate of number of label issues when you don't need the indices of the label issues. + + Note: ``'off_diagonal'`` may sometimes underestimate issues for data with few classes, so consider using ``'off_diagonal_calibrated'`` instead if your data has < 4 classes. multi_label : bool, optional Set ``False`` if your dataset is for regular (multi-class) classification, where each example belongs to exactly one class. @@ -113,7 +121,15 @@ def num_label_issues( num_issues : The estimated number of examples with label issues in the dataset. """ - valid_methods = ["off_diagonal", "off_diagonal_calibrated"] + valid_methods = ["off_diagonal", "off_diagonal_calibrated", "off_diagonal_custom"] + if isinstance(confident_joint, np.ndarray) and estimation_method != "off_diagonal_custom": + warn_str = ( + "The supplied `confident_joint` is ignored as `confident_joint` is recomuputed internally using " + "the supplied `labels` and `pred_probs`. If you still want to use custom `confident_joint` call function " + "with `estimation_method='off_diagonal_custom'`." + ) + warnings.warn(warn_str) + if multi_label: return _num_label_issues_multilabel( labels=labels, @@ -123,29 +139,54 @@ def num_label_issues( labels = labels_to_array(labels) assert_valid_inputs(X=None, y=labels, pred_probs=pred_probs) - if confident_joint is None: - # Original non-calibrated counts of confidently correctly and incorrectly labeled examples. - computed_confident_joint = compute_confident_joint( - labels=labels, pred_probs=pred_probs, calibrate=False + if estimation_method == "off_diagonal": + _, cl_error_indices = compute_confident_joint( + labels=labels, + pred_probs=pred_probs, + calibrate=False, + return_indices_of_off_diagonals=True, ) - else: - computed_confident_joint = confident_joint - assert isinstance(computed_confident_joint, np.ndarray) + label_issues_mask = np.zeros(len(labels), dtype=bool) + for idx in cl_error_indices: + label_issues_mask[idx] = True - if estimation_method == "off_diagonal": - num_issues: int = np.sum(computed_confident_joint) - np.trace(computed_confident_joint) + # Remove label issues if given label == model prediction + pred = pred_probs.argmax(axis=1) + for i, pred_label in enumerate(pred): + if pred_label == labels[i]: + label_issues_mask[i] = False + num_issues = np.sum(label_issues_mask) elif estimation_method == "off_diagonal_calibrated": + calculated_confident_joint = compute_confident_joint( + labels=labels, + pred_probs=pred_probs, + calibrate=True, + ) + assert isinstance(calculated_confident_joint, np.ndarray) # Estimate_joint calibrates the row sums to match the prior distribution of given labels and normalizes to sum to 1 - joint = estimate_joint(labels, pred_probs, confident_joint=computed_confident_joint) + joint = estimate_joint(labels, pred_probs, confident_joint=calculated_confident_joint) frac_issues = 1.0 - joint.trace() num_issues = np.rint(frac_issues * len(labels)).astype(int) + elif estimation_method == "off_diagonal_custom": + if not isinstance(confident_joint, np.ndarray): + raise ValueError( + f""" + No `confident_joint` provided. For 'estimation_method' = {estimation_method} you need to provide pre-calculated + `confident_joint` matrix. Use a different `estimation_method` if you want the `confident_joint` matrix to + be calculated for you. + """ + ) + else: + joint = estimate_joint(labels, pred_probs, confident_joint=confident_joint) + frac_issues = 1.0 - joint.trace() + num_issues = np.rint(frac_issues * len(labels)).astype(int) else: raise ValueError( f""" - {estimation_method} is not a valid estimation method! - Please choose a valid estimation method: {valid_methods} - """ + {estimation_method} is not a valid estimation method! + Please choose a valid estimation method: {valid_methods} + """ ) return num_issues @@ -169,12 +210,19 @@ def _num_label_issues_multilabel( ------- num_issues : int The estimated number of examples with label issues in the multi-label dataset. + + Note: We set the filter_by method as 'confident_learning' to match the non-multilabel case + (analog to the off_diagonal estimation method) """ from cleanlab.filter import find_label_issues issues_idx = find_label_issues( - labels=labels, pred_probs=pred_probs, confident_joint=confident_joint, multi_label=True + labels=labels, + pred_probs=pred_probs, + confident_joint=confident_joint, + multi_label=True, + filter_by="confident_learning", # specified to match num_label_issues ) return sum(issues_idx) @@ -341,7 +389,12 @@ def estimate_joint( multi_label=multi_label, ) else: - calibrated_cj = calibrate_confident_joint(confident_joint, labels, multi_label=multi_label) + if labels is not None: + calibrated_cj = calibrate_confident_joint( + confident_joint, labels, multi_label=multi_label + ) + else: + calibrated_cj = confident_joint assert isinstance(calibrated_cj, np.ndarray) if multi_label: @@ -1402,7 +1455,10 @@ def get_confident_thresholds( np.mean(pred_probs[:, k][labels == k]) if k in unique_classes else BIG_VALUE for k in all_classes ] - return np.array(confident_thresholds) + confident_thresholds = np.clip( + confident_thresholds, a_min=CONFIDENT_THRESHOLDS_LOWER_BOUND, a_max=None + ) + return confident_thresholds def _get_confident_thresholds_multilabel( diff --git a/cleanlab/datalab/__init__.py b/cleanlab/datalab/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cleanlab/datalab/data.py b/cleanlab/datalab/data.py new file mode 100644 index 0000000000..e04a84c1a6 --- /dev/null +++ b/cleanlab/datalab/data.py @@ -0,0 +1,313 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . +"""Classes and methods for datasets that are loaded into Datalab.""" + +import os +from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Union, cast, TYPE_CHECKING + +try: + import datasets +except ImportError as error: + raise ImportError( + "Cannot import datasets package. " + "Please install it and try again, or just install cleanlab with " + "all optional dependencies via: `pip install 'cleanlab[all]'`" + ) from error +import numpy as np +import pandas as pd +from datasets.arrow_dataset import Dataset +from datasets import ClassLabel + +from cleanlab.internal.validation import labels_to_array + + +if TYPE_CHECKING: # pragma: no cover + DatasetLike = Union[Dataset, pd.DataFrame, Dict[str, Any], List[Dict[str, Any]], str] + + +class DataFormatError(ValueError): + """Exception raised when the data is not in a supported format.""" + + def __init__(self, data: Any): + self.data = data + message = ( + f"Unsupported data type: {type(data)}\n" + "Supported types: " + "datasets.Dataset, pandas.DataFrame, dict, list, str" + ) + super().__init__(message) + + +class DatasetDictError(ValueError): + """Exception raised when a DatasetDict is passed to Datalab. + + Usually, this means that a dataset identifier was passed to Datalab, but + the dataset is a DatasetDict, which contains multiple splits of the dataset. + + """ + + def __init__(self): + message = ( + "Please pass a single dataset, not a DatasetDict. " + "Try specifying a split, e.g. `dataset = load_dataset('dataset', split='train')` " + "then pass `dataset` to Datalab." + ) + super().__init__(message) + + +class DatasetLoadError(ValueError): + """Exception raised when a dataset cannot be loaded. + + Parameters + ---------- + dataset_type: type + The type of dataset that failed to load. + """ + + def __init__(self, dataset_type: type): + message = f"Failed to load dataset from {dataset_type}.\n" + super().__init__(message) + + +class Data: + """ + Class that holds and validates datasets for Datalab. + + Internally, the data is stored as a datasets.Dataset object and the labels + are integers (ranging from 0 to K-1, where K is the number of classes) stored + in a numpy array. + + Parameters + ---------- + data : + Dataset to be audited by Datalab. + Several formats are supported, which will internally be converted to a Dataset object. + + Supported formats: + - datasets.Dataset + - pandas.DataFrame + - dict + - keys are strings + - values are arrays or lists of equal length + - list + - list of dictionaries with the same keys + - str + - path to a local file + - Text (.txt) + - CSV (.csv) + - JSON (.json) + - or a dataset identifier on the Hugging Face Hub + It checks if the string is a path to a file that exists locally, and if not, + it assumes it is a dataset identifier on the Hugging Face Hub. + + label_name : Union[str, List[str]] + Name of the label column in the dataset. + + Warnings + -------- + Optional dependencies: + + - datasets : + Dataset, DatasetDict and load_dataset are imported from datasets. + This is an optional dependency of cleanlab, but is required for + :py:class:`Datalab ` to work. + """ + + def __init__(self, data: "DatasetLike", label_name: Optional[str] = None) -> None: + self._validate_data(data) + self._data = self._load_data(data) + self._data_hash = hash(self._data) + self.labels = Label(data=self._data, label_name=label_name) + + def _load_data(self, data: "DatasetLike") -> Dataset: + """Checks the type of dataset and uses the correct loader method and + assigns the result to the data attribute.""" + dataset_factory_map: Dict[type, Callable[..., Dataset]] = { + Dataset: lambda x: x, + pd.DataFrame: Dataset.from_pandas, + dict: self._load_dataset_from_dict, + list: self._load_dataset_from_list, + str: self._load_dataset_from_string, + } + if not isinstance(data, tuple(dataset_factory_map.keys())): + raise DataFormatError(data) + return dataset_factory_map[type(data)](data) + + def __len__(self) -> int: + return len(self._data) + + def __eq__(self, other) -> bool: + if isinstance(other, Data): + # Equality checks + hashes_are_equal = self._data_hash == other._data_hash + labels_are_equal = self.labels == other.labels + return all([hashes_are_equal, labels_are_equal]) + return False + + def __hash__(self) -> int: + return self._data_hash + + @property + def class_names(self) -> List[str]: + return self.labels.class_names + + @property + def has_labels(self) -> bool: + """Check if labels are available.""" + return self.labels.is_available + + @staticmethod + def _validate_data(data) -> None: + if isinstance(data, datasets.DatasetDict): + raise DatasetDictError() + if not isinstance(data, (Dataset, pd.DataFrame, dict, list, str)): + raise DataFormatError(data) + + @staticmethod + def _load_dataset_from_dict(data_dict: Dict[str, Any]) -> Dataset: + try: + return Dataset.from_dict(data_dict) + except Exception as error: + raise DatasetLoadError(dict) from error + + @staticmethod + def _load_dataset_from_list(data_list: List[Dict[str, Any]]) -> Dataset: + try: + return Dataset.from_list(data_list) + except Exception as error: + raise DatasetLoadError(list) from error + + @staticmethod + def _load_dataset_from_string(data_string: str) -> Dataset: + if not os.path.exists(data_string): + try: + dataset = datasets.load_dataset(data_string) + return cast(Dataset, dataset) + except Exception as error: + raise DatasetLoadError(str) from error + + factory: Dict[str, Callable[[str], Any]] = { + ".txt": Dataset.from_text, + ".csv": Dataset.from_csv, + ".json": Dataset.from_json, + } + + extension = os.path.splitext(data_string)[1] + if extension not in factory: + raise DatasetLoadError(type(data_string)) + + dataset = factory[extension](data_string) + dataset_cast = cast(Dataset, dataset) + return dataset_cast + + +class Label: + """ + Class to represent labels in a dataset. + + Parameters + ---------- + """ + + def __init__(self, *, data: Dataset, label_name: Optional[str] = None) -> None: + self._data = data + self.label_name = label_name + self.labels = labels_to_array([]) + self.label_map: Mapping[str, Any] = {} + if label_name is not None: + self.labels, self.label_map = _extract_labels(data, label_name) + self._validate_labels() + + def __len__(self) -> int: + if self.labels is None: + return 0 + return len(self.labels) + + def __eq__(self, __value: object) -> bool: + if isinstance(__value, Label): + labels_are_equal = np.array_equal(self.labels, __value.labels) + names_are_equal = self.label_name == __value.label_name + maps_are_equal = self.label_map == __value.label_map + return all([labels_are_equal, names_are_equal, maps_are_equal]) + return False + + def __getitem__(self, __index: Union[int, slice, np.ndarray]) -> np.ndarray: + return self.labels[__index] + + def __bool__(self) -> bool: + return self.is_available + + @property + def class_names(self) -> List[str]: + """A list of class names that are present in the dataset. + + Without labels, this will return an empty list. + """ + return list(self.label_map.values()) + + @property + def is_available(self) -> bool: + """Check if labels are available.""" + empty_labels = self.labels is None or len(self.labels) == 0 + empty_label_map = self.label_map is None or len(self.label_map) == 0 + return not (empty_labels or empty_label_map) + + def _validate_labels(self) -> None: + if self.label_name not in self._data.column_names: + raise ValueError(f"Label column '{self.label_name}' not found in dataset.") + labels = self._data[self.label_name] + assert isinstance(labels, (np.ndarray, list)) + assert len(labels) == len(self._data) + + +def _extract_labels(data: Dataset, label_name: str) -> Tuple[np.ndarray, Mapping]: + """ + Picks out labels from the dataset and formats them to be [0, 1, ..., K-1] + where K is the number of classes. Also returns a mapping from the formatted + labels to the original labels in the dataset. + + Note: This function is not meant to be used directly. It is used by + ``cleanlab.data.Data`` to extract the formatted labels from the dataset + and stores them as attributes. + + Parameters + ---------- + label_name : str + Name of the column in the dataset that contains the labels. + + Returns + ------- + formatted_labels : np.ndarray + Labels in the format [0, 1, ..., K-1] where K is the number of classes. + + inverse_map : dict + Mapping from the formatted labels to the original labels in the dataset. + """ + + labels = labels_to_array(data[label_name]) # type: ignore[assignment] + if labels.ndim != 1: + raise ValueError("labels must be 1D numpy array.") + + label_name_feature = data.features[label_name] + if isinstance(label_name_feature, ClassLabel): + label_map = {label: label_name_feature.str2int(label) for label in label_name_feature.names} + formatted_labels = labels + else: + label_map = {label: i for i, label in enumerate(np.unique(labels))} + formatted_labels = np.vectorize(label_map.get)(labels) + inverse_map = {i: label for label, i in label_map.items()} + + return formatted_labels, inverse_map diff --git a/cleanlab/datalab/data_issues.py b/cleanlab/datalab/data_issues.py new file mode 100644 index 0000000000..89c1663a98 --- /dev/null +++ b/cleanlab/datalab/data_issues.py @@ -0,0 +1,291 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . +""" +Module for the :py:class:`DataIssues` class, which serves as a central repository for storing +information and statistics about issues found in a dataset. + +It collects information from various +:py:class:`IssueManager ` +instances and keeps track of each issue, a summary for each type of issue, +related information and statistics about the issues. + +The collected information can be accessed using the +:py:meth:`get_info ` method. +""" +from __future__ import annotations + +import warnings +from typing import TYPE_CHECKING, Any, Dict, Optional +import numpy as np + +import pandas as pd + +if TYPE_CHECKING: # pragma: no cover + from cleanlab.datalab.data import Data + from cleanlab.datalab.issue_manager import IssueManager + + +class DataIssues: + """ + Class that collects and stores information and statistics on issues found in a dataset. + + Parameters + ---------- + data : + The data object for which the issues are being collected. + + Parameters + ---------- + issues : pd.DataFrame + Stores information about each individual issue found in the data, + on a per-example basis. + issue_summary : pd.DataFrame + Summarizes the overall statistics for each issue type. + info : dict + A dictionary that contains information and statistics about the data and each issue type. + """ + + def __init__(self, data: Data) -> None: + self.issues: pd.DataFrame = pd.DataFrame(index=range(len(data))) + self.issue_summary: pd.DataFrame = pd.DataFrame( + columns=["issue_type", "score", "num_issues"] + ).astype({"score": np.float64, "num_issues": np.int64}) + self.info: Dict[str, Dict[str, Any]] = { + "statistics": get_data_statistics(data), + } + self._label_map = data.labels.label_map + + @property + def statistics(self) -> Dict[str, Any]: + """Returns the statistics dictionary. + + Shorthand for self.info["statistics"]. + """ + return self.info["statistics"] + + def get_issues(self, issue_name: Optional[str] = None) -> pd.DataFrame: + """ + Use this after finding issues to see which examples suffer from which types of issues. + + Parameters + ---------- + issue_name : str or None + The type of issue to focus on. If `None`, returns full DataFrame summarizing all of the types of issues detected in each example from the dataset. + + Raises + ------ + ValueError + If `issue_name` is not a type of issue previously considered in the audit. + + Returns + ------- + specific_issues : + A DataFrame where each row corresponds to an example from the dataset and columns specify: + whether this example exhibits a particular type of issue and how severely (via a numeric quality score where lower values indicate more severe instances of the issue). + + Additional columns may be present in the DataFrame depending on the type of issue specified. + """ + if issue_name is None: + return self.issues + + columns = [col for col in self.issues.columns if issue_name in col] + if not columns: + raise ValueError(f"No columns found for issue type '{issue_name}'.") + specific_issues = self.issues[columns] + info = self.get_info(issue_name=issue_name) + if issue_name == "label": + specific_issues = specific_issues.assign( + given_label=info["given_label"], predicted_label=info["predicted_label"] + ) + + if issue_name == "near_duplicate": + column_dict = { + k: info.get(k) + for k in ["near_duplicate_sets", "distance_to_nearest_neighbor"] + if info.get(k) is not None + } + specific_issues = specific_issues.assign(**column_dict) + return specific_issues + + def get_issue_summary(self, issue_name: Optional[str] = None) -> pd.DataFrame: + """Summarize the issues found in dataset of a particular type, + including how severe this type of issue is overall across the dataset. + + Parameters + ---------- + issue_name : + Name of the issue type to summarize. If `None`, summarizes each of the different issue types previously considered in the audit. + + Returns + ------- + issue_summary : + DataFrame where each row corresponds to a type of issue, and columns quantify: + the number of examples in the dataset estimated to exhibit this type of issue, + and the overall severity of the issue across the dataset (via a numeric quality score where lower values indicate that the issue is overall more severe). + """ + if self.issue_summary.empty: + raise ValueError( + "No issues found in the dataset. " + "Call `find_issues` before calling `get_issue_summary`." + ) + + if issue_name is None: + return self.issue_summary + + row_mask = self.issue_summary["issue_type"] == issue_name + if not any(row_mask): + raise ValueError(f"Issue type {issue_name} not found in the summary.") + return self.issue_summary[row_mask].reset_index(drop=True) + + def get_info(self, issue_name: Optional[str] = None) -> Dict[str, Any]: + """Get the info for the issue_name key. + + This function is used to get the info for a specific issue_name. If the info is not computed yet, it will raise an error. + + Parameters + ---------- + issue_name : + The issue name for which the info is required. + + Returns + ------- + info: + The info for the issue_name. + """ + info = self.info.get(issue_name, None) if issue_name else self.info + if info is None: + raise ValueError( + f"issue_name {issue_name} not found in self.info. These have not been computed yet." + ) + info = info.copy() + if issue_name == "label": + if self._label_map is None: + raise ValueError( + "The label map is not available. " + "Most likely, no label column was provided when creating the Data object." + ) + # Labels that are stored as integers may need to be converted to strings. + for key in ["given_label", "predicted_label"]: + labels = info.get(key, None) + if labels is not None: + info[key] = np.vectorize(self._label_map.get)(labels) + + info["class_names"] = self.statistics["class_names"] + return info + + def collect_statistics_from_issue_manager(self, issue_manager: IssueManager) -> None: + """Update the statistics in the info dictionary. + + Parameters + ---------- + statistics : + A dictionary of statistics to add/update in the info dictionary. + + Examples + -------- + + A common use case is to reuse the KNN-graph across multiple issue managers. + To avoid recomputing the KNN-graph for each issue manager, + we can pass it as a statistic to the issue managers. + + >>> from scipy.sparse import csr_matrix + >>> weighted_knn_graph = csr_matrix(...) + >>> issue_manager_that_computes_knn_graph = ... + + """ + key = "statistics" + statistics: Dict[str, Any] = issue_manager.info.pop(key, {}) + if statistics: + self.info[key].update(statistics) + + def collect_results_from_issue_manager(self, issue_manager: IssueManager) -> None: + """ + Collects results from an IssueManager and update the corresponding + attributes of the Datalab object. + + This includes: + - self.issues + - self.issue_summary + - self.info + + Parameters + ---------- + issue_manager : + IssueManager object to collect results from. + """ + overlapping_columns = list(set(self.issues.columns) & set(issue_manager.issues.columns)) + if overlapping_columns: + warnings.warn( + f"Overwriting columns {overlapping_columns} in self.issues with " + f"columns from issue manager {issue_manager}." + ) + self.issues.drop(columns=overlapping_columns, inplace=True) + self.issues = self.issues.join(issue_manager.issues, how="outer") + + if issue_manager.issue_name in self.issue_summary["issue_type"].values: + warnings.warn( + f"Overwriting row in self.issue_summary with " + f"row from issue manager {issue_manager}." + ) + self.issue_summary = self.issue_summary[ + self.issue_summary["issue_type"] != issue_manager.issue_name + ] + issue_column_name: str = f"is_{issue_manager.issue_name}_issue" + num_issues: int = int(issue_manager.issues[issue_column_name].sum()) + self.issue_summary = pd.concat( + [ + self.issue_summary, + issue_manager.summary.assign(num_issues=num_issues), + ], + axis=0, + ignore_index=True, + ) + + if issue_manager.issue_name in self.info: + warnings.warn( + f"Overwriting key {issue_manager.issue_name} in self.info with " + f"key from issue manager {issue_manager}." + ) + self.info[issue_manager.issue_name] = issue_manager.info + + def set_health_score(self) -> None: + """Set the health score for the dataset based on the issue summary. + + Currently, the health score is the mean of the scores for each issue type. + """ + self.info["statistics"]["health_score"] = self.issue_summary["score"].mean() + + +def get_data_statistics(data: Data) -> Dict[str, Any]: + """Get statistics about a dataset. + + This function is called to initialize the "statistics" info in all `Datalab` objects. + + Parameters + ---------- + data : Data + Data object containing the dataset. + """ + statistics: Dict[str, Any] = { + "num_examples": len(data), + "multi_label": False, + "health_score": None, + } + if data.labels.is_available: + class_names = data.class_names + statistics["class_names"] = class_names + statistics["num_classes"] = len(class_names) + return statistics diff --git a/cleanlab/datalab/datalab.py b/cleanlab/datalab/datalab.py new file mode 100644 index 0000000000..d48536f9b4 --- /dev/null +++ b/cleanlab/datalab/datalab.py @@ -0,0 +1,531 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . +""" +Datalab offers a unified audit to detect all kinds of issues in data and labels. + +.. note:: + .. include:: optional_dependencies.rst +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union + +import numpy as np +import pandas as pd + +import cleanlab +from cleanlab.datalab.data import Data +from cleanlab.datalab.data_issues import DataIssues +from cleanlab.datalab.display import _Displayer +from cleanlab.datalab.issue_finder import IssueFinder +from cleanlab.datalab.serialize import _Serializer +from cleanlab.datalab.report import Reporter + +if TYPE_CHECKING: # pragma: no cover + import numpy.typing as npt + from datasets.arrow_dataset import Dataset + from scipy.sparse import csr_matrix + + DatasetLike = Union[Dataset, pd.DataFrame, Dict[str, Any], List[Dict[str, Any]], str] + +__all__ = ["Datalab"] + + +class Datalab: + """ + A single object to automatically detect all kinds of issues in datasets. + This is how we recommend you interface with the cleanlab library if you want to audit the quality of your data and detect issues within it. + If you have other specific goals (or are doing a less standard ML task not supported by Datalab), then consider using the other methods across the library. + Datalab tracks intermediate state (e.g. data statistics) from certain cleanlab functions that can be re-used across other cleanlab functions for better efficiency. + + Parameters + ---------- + data : Union[Dataset, pd.DataFrame, dict, list, str] + Dataset-like object that can be converted to a Hugging Face Dataset object. + + It should contain the labels for all examples, identified by a + `label_name` column in the Dataset object. + + Supported formats: + - datasets.Dataset + - pandas.DataFrame + - dict (keys are strings, values are arrays/lists of length ``N``) + - list (list of dictionaries that each have the same keys) + - str + + - path to a local file: Text (.txt), CSV (.csv), JSON (.json) + - or a dataset identifier on the Hugging Face Hub + + label_name : str + The name of the label column in the dataset. + + verbosity : int, optional + The higher the verbosity level, the more information + Datalab prints when auditing a dataset. + Valid values are 0 through 4. Default is 1. + + Examples + -------- + >>> import datasets + >>> from cleanlab import Datalab + >>> data = datasets.load_dataset("glue", "sst2", split="train") + >>> datalab = Datalab(data, label_name="label") + """ + + def __init__( + self, + data: "DatasetLike", + label_name: Optional[str] = None, + verbosity: int = 1, + ) -> None: + self._data = Data(data, label_name) + self.data = self._data._data + self._labels = self._data.labels + self._label_map = self._labels.label_map + self.label_name = self._labels.label_name + self._data_hash = self._data._data_hash + self.data_issues = DataIssues(self._data) + self.cleanlab_version = cleanlab.version.__version__ + self.verbosity = verbosity + + def __repr__(self) -> str: + return _Displayer(data_issues=self.data_issues).__repr__() + + def __str__(self) -> str: + return _Displayer(data_issues=self.data_issues).__str__() + + @property + def labels(self) -> np.ndarray: + """Labels of the dataset, in a [0, 1, ..., K-1] format.""" + return self._labels.labels + + @property + def has_labels(self) -> bool: + """Whether the dataset has labels.""" + return self._labels.is_available + + @property + def class_names(self) -> List[str]: + """Names of the classes in the dataset. + + If the dataset has no labels, returns an empty list. + """ + return self._labels.class_names + + def find_issues( + self, + *, + pred_probs: Optional[np.ndarray] = None, + features: Optional[npt.NDArray] = None, + knn_graph: Optional[csr_matrix] = None, + issue_types: Optional[Dict[str, Any]] = None, + ) -> None: + """ + Checks the dataset for all sorts of common issues in real-world data (in both labels and feature values). + + You can use Datalab to find issues in your data, utilizing *any* model you have already trained. + This method only interacts with your model via its predictions or embeddings (and other functions thereof). + The more of these inputs you provide, the more types of issues Datalab can detect in your dataset/labels. + If you provide a subset of these inputs, Datalab will output what insights it can based on the limited information from your model. + + Note + ---- + This method acts as a wrapper around the :py:meth:`IssueFinder.find_issues ` method, + where the core logic for issue detection is implemented. + + Note + ---- + The issues are saved in the ``self.issues`` attribute, but are not returned. + + Parameters + ---------- + pred_probs : + Out-of-sample predicted class probabilities made by the model for every example in the dataset. + To best detect label issues, provide this input obtained from the most accurate model you can produce. + + If provided, this must be a 2D array with shape (num_examples, K) where K is the number of classes in the dataset. + + features : Optional[np.ndarray] + Feature embeddings (vector representations) of every example in the dataset. + + If provided, this must be a 2D array with shape (num_examples, num_features). + + knn_graph : + Sparse matrix representing distances between examples in the dataset in a k nearest neighbor graph. + + If provided, this must be a square CSR matrix with shape (num_examples, num_examples) and (k*num_examples) non-zero entries (k is the number of nearest neighbors considered for each example) + evenly distributed across the rows. + The non-zero entries must be the distances between the corresponding examples. Self-distances must be omitted + (i.e. the diagonal must be all zeros and the k nearest neighbors of each example must not include itself). + + For any duplicated examples i,j whose distance is 0, there should be an *explicit* zero stored in the matrix, i.e. ``knn_graph[i,j] = 0``. + + If both `knn_graph` and `features` are provided, the `knn_graph` will take precendence. + If `knn_graph` is not provided, it is constructed based on the provided `features`. + If neither `knn_graph` nor `features` are provided, certain issue types like (near) duplicates will not be considered. + + issue_types : + Collection specifying which types of issues to consider in audit and any non-default parameter settings to use. + If unspecified, a default set of issue types and recommended parameter settings is considered. + + This is a dictionary of dictionaries, where the keys are the issue types of interest + and the values are dictionaries of parameter values that control how each type of issue is detected (only for advanced users). + More specifically, the values are constructor keyword arguments passed to the corresponding ``IssueManager``, + which is responsible for detecting the particular issue type. + + .. seealso:: + :py:class:`IssueManager ` + + Examples + -------- + + Here are some ways to provide inputs to :py:meth:`find_issues`: + + - Passing ``pred_probs``: + .. code-block:: python + + >>> from sklearn.linear_model import LogisticRegression + >>> import numpy as np + >>> from cleanlab import Datalab + >>> X = np.array([[0, 1], [1, 1], [2, 2], [2, 0]]) + >>> y = np.array([0, 1, 1, 0]) + >>> clf = LogisticRegression(random_state=0).fit(X, y) + >>> pred_probs = clf.predict_proba(X) + >>> lab = Datalab(data={"X": X, "y": y}, label_name="y") + >>> lab.find_issues(pred_probs=pred_probs) + + + - Passing ``features``: + .. code-block:: python + + >>> from sklearn.linear_model import LogisticRegression + >>> from sklearn.neighbors import NearestNeighbors + >>> import numpy as np + >>> from cleanlab import Datalab + >>> X = np.array([[0, 1], [1, 1], [2, 2], [2, 0]]) + >>> y = np.array([0, 1, 1, 0]) + >>> lab = Datalab(data={"X": X, "y": y}, label_name="y") + >>> lab.find_issues(features=X) + + .. note:: + + You can pass both ``pred_probs`` and ``features`` to :py:meth:`find_issues` for a more comprehensive audit. + + - Passing a ``knn_graph``: + .. code-block:: python + + >>> from sklearn.neighbors import NearestNeighbors + >>> import numpy as np + >>> from cleanlab import Datalab + >>> X = np.array([[0, 1], [1, 1], [2, 2], [2, 0]]) + >>> y = np.array([0, 1, 1, 0]) + >>> nbrs = NearestNeighbors(n_neighbors=2, metric="euclidean").fit(X) + >>> knn_graph = nbrs.kneighbors_graph(mode="distance") + >>> knn_graph # Pass this to Datalab + <4x4 sparse matrix of type '' + with 8 stored elements in Compressed Sparse Row format> + >>> knn_graph.toarray() # DO NOT PASS knn_graph.toarray() to Datalab, only pass the sparse matrix itself + array([[0. , 1. , 2.23606798, 0. ], + [1. , 0. , 1.41421356, 0. ], + [0. , 1.41421356, 0. , 2. ], + [0. , 1.41421356, 2. , 0. ]]) + >>> lab = Datalab(data={"X": X, "y": y}, label_name="y") + >>> lab.find_issues(knn_graph=knn_graph) + + - Configuring issue types: + Suppose you want to only consider label issues. Just pass a dictionary with the key "label" and an empty dictionary as the value (to use default label issue parameters). + + .. code-block:: python + + >>> issue_types = {"label": {}} + >>> # lab.find_issues(pred_probs=pred_probs, issue_types=issue_types) + + If you are advanced user who wants greater control, you can pass keyword arguments to the issue manager that handles the label issues. + For example, if you want to pass the keyword argument "clean_learning_kwargs" + to the constructor of the :py:class:`LabelIssueManager `, you would pass: + + + .. code-block:: python + + >>> issue_types = { + ... "label": { + ... "clean_learning_kwargs": { + ... "prune_method": "prune_by_noise_rate", + ... }, + ... }, + ... } + >>> # lab.find_issues(pred_probs=pred_probs, issue_types=issue_types) + + """ + issue_finder = IssueFinder(datalab=self, verbosity=self.verbosity) + issue_finder.find_issues( + pred_probs=pred_probs, + features=features, + knn_graph=knn_graph, + issue_types=issue_types, + ) + + def report( + self, + *, + num_examples: int = 5, + verbosity: Optional[int] = None, + include_description: bool = True, + show_summary_score: bool = False, + ) -> None: + """Prints informative summary of all issues. + + Parameters + ---------- + num_examples : + Number of examples to show for each type of issue. + The report shows the top `num_examples` instances in the dataset that suffer the most from each type of issue. + + verbosity : + Higher verbosity levels add more information to the report. + + include_description : + Whether or not to include a description of each issue type in the report. + Consider setting this to ``False`` once you're familiar with how each issue type is defined. + + See Also + -------- + For advanced usage, see documentation for the + :py:class:`Reporter ` class. + """ + if verbosity is None: + verbosity = self.verbosity + reporter = Reporter( + data_issues=self.data_issues, + verbosity=verbosity, + include_description=include_description, + show_summary_score=show_summary_score, + ) + reporter.report(num_examples=num_examples) + + @property + def issues(self) -> pd.DataFrame: + """Issues found in each example from the dataset.""" + return self.data_issues.issues + + @issues.setter + def issues(self, issues: pd.DataFrame) -> None: + self.data_issues.issues = issues + + @property + def issue_summary(self) -> pd.DataFrame: + """Summary of issues found in the dataset and the overall severity of each type of issue. + + This is a wrapper around the ``DataIssues.issue_summary`` attribute. + + Examples + ------- + + If checks for "label" and "outlier" issues were run, + then the issue summary will look something like this: + + >>> datalab.issue_summary + issue_type score + outlier 0.123 + label 0.456 + """ + return self.data_issues.issue_summary + + @issue_summary.setter + def issue_summary(self, issue_summary: pd.DataFrame) -> None: + self.data_issues.issue_summary = issue_summary + + @property + def info(self) -> Dict[str, Dict[str, Any]]: + """Information and statistics about the dataset issues found. + + This is a wrapper around the ``DataIssues.info`` attribute. + + Examples + ------- + + If checks for "label" and "outlier" issues were run, + then the info will look something like this: + + >>> datalab.info + { + "label": { + "given_labels": [0, 1, 0, 1, 1, 1, 1, 1, 0, 1, ...], + "predicted_label": [0, 0, 0, 1, 0, 1, 0, 1, 0, 1, ...], + ..., + }, + "outlier": { + "nearest_neighbor": [3, 7, 1, 2, 8, 4, 5, 9, 6, 0, ...], + "distance_to_nearest_neighbor": [0.123, 0.789, 0.456, ...], + ..., + }, + } + """ + return self.data_issues.info + + @info.setter + def info(self, info: Dict[str, Dict[str, Any]]) -> None: + self.data_issues.info = info + + def get_issues(self, issue_name: Optional[str] = None) -> pd.DataFrame: + """ + Use this after finding issues to see which examples suffer from which types of issues. + + NOTE + ---- + This is a wrapper around the :py:meth:`DataIssues.get_issues ` method. + + Parameters + ---------- + issue_name : str or None + The type of issue to focus on. If `None`, returns full DataFrame summarizing all of the types of issues detected in each example from the dataset. + + Raises + ------ + ValueError + If `issue_name` is not a type of issue previously considered in the audit. + + Returns + ------- + specific_issues : + A DataFrame where each row corresponds to an example from the dataset and columns specify: + whether this example exhibits a particular type of issue, and how severely (via a numeric quality score where lower values indicate more severe instances of the issue). + The quality scores lie between 0-1 and are directly comparable between examples (for the same issue type), but not across different issue types. + + Additional columns may be present in the DataFrame depending on the type of issue specified. + """ + return self.data_issues.get_issues(issue_name=issue_name) + + def get_issue_summary(self, issue_name: Optional[str] = None) -> pd.DataFrame: + """Summarize the issues found in dataset of a particular type, + including how severe this type of issue is overall across the dataset. + + NOTE + ---- + This is a wrapper around the + :py:meth:`DataIssues.get_issue_summary ` method. + + Parameters + ---------- + issue_name : + Name of the issue type to summarize. If `None`, summarizes each of the different issue types previously considered in the audit. + + Returns + ------- + issue_summary : + DataFrame where each row corresponds to a type of issue, and columns quantify: + the number of examples in the dataset estimated to exhibit this type of issue, + and the overall severity of the issue across the dataset (via a numeric quality score where lower values indicate that the issue is overall more severe). + The quality scores lie between 0-1 and are directly comparable between multiple datasets (for the same issue type), but not across different issue types. + """ + return self.data_issues.get_issue_summary(issue_name=issue_name) + + def get_info(self, issue_name: Optional[str] = None) -> Dict[str, Any]: + """Get the info for the issue_name key. + + This function is used to get the info for a specific issue_name. If the info is not computed yet, it will raise an error. + + NOTE + ---- + This is a wrapper around the + :py:meth:`DataIssues.get_info ` method. + + Parameters + ---------- + issue_name : + The issue name for which the info is required. + + Returns + ------- + :py:meth:`info ` : + The info for the issue_name. + """ + return self.data_issues.get_info(issue_name) + + @staticmethod + def list_possible_issue_types() -> List[str]: + """Returns a list of all registered issue types. + + Any issue type that is not in this list cannot be used in the :py:meth:`find_issues` method. + + Note + ---- + This method is a wrapper around :py:meth:`IssueFinder.list_possible_issue_types `. + + See Also + -------- + :py:class:`REGISTRY ` : All available issue types and their corresponding issue managers can be found here. + """ + return IssueFinder.list_possible_issue_types() + + @staticmethod + def list_default_issue_types() -> List[str]: + """Returns a list of the issue types that are run by default + when :py:meth:`find_issues` is called without specifying `issue_types`. + + Note + ---- + This method is a wrapper around :py:meth:`IssueFinder.list_default_issue_types `. + + See Also + -------- + :py:class:`REGISTRY ` : All available issue types and their corresponding issue managers can be found here. + """ + return IssueFinder.list_default_issue_types() + + def save(self, path: str, force: bool = False) -> None: + """Saves this Datalab object to file (all files are in folder at `path/`). + We do not guarantee saved Datalab can be loaded from future versions of cleanlab. + + Parameters + ---------- + path : + Folder in which all information about this Datalab should be saved. + + force : + If ``True``, overwrites any existing files in the folder at `path`. Use this with caution! + + Note + ---- + You have to save the Dataset yourself separately if you want it saved to file. + """ + _Serializer.serialize(path=path, datalab=self, force=force) + save_message = f"Saved Datalab to folder: {path}" + print(save_message) + + @staticmethod + def load(path: str, data: Optional[Dataset] = None) -> "Datalab": + """Loads Datalab object from a previously saved folder. + + Parameters + ---------- + `path` : + Path to the folder previously specified in ``Datalab.save()``. + + `data` : + The dataset used to originally construct the Datalab. + Remember the dataset is not saved as part of the Datalab, + you must save/load the data separately. + + Returns + ------- + `datalab` : + A Datalab object that is identical to the one originally saved. + """ + datalab = _Serializer.deserialize(path=path, data=data) + load_message = f"Datalab loaded from folder: {path}" + print(load_message) + return datalab diff --git a/cleanlab/datalab/display.py b/cleanlab/datalab/display.py new file mode 100644 index 0000000000..520b261516 --- /dev/null +++ b/cleanlab/datalab/display.py @@ -0,0 +1,61 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . +""" +Module that handles the string representation of Datalab objects. +""" + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: # pragma: no cover + from cleanlab.datalab.data_issues import DataIssues + + +class _Displayer: + def __init__(self, data_issues: "DataIssues") -> None: + self.data_issues = data_issues + + def __repr__(self) -> str: + """What is displayed in console if user executes: >>> datalab""" + checks_run = not self.data_issues.issues.empty + display_str = f"checks_run={checks_run}" + num_examples = self.data_issues.get_info("statistics")["num_examples"] + if num_examples is not None: + display_str += f", num_examples={num_examples}" + num_classes = self.data_issues.get_info("statistics")["num_classes"] + if num_classes is not None: + display_str += f", num_classes={num_classes}" + if checks_run: + issues_identified = self.data_issues.issue_summary["num_issues"].sum() + display_str += f", issues_identified={issues_identified}" + return f"Datalab({display_str})" + + def __str__(self) -> str: + """What is displayed if user executes: print(datalab)""" + checks_run = not self.data_issues.issues.empty + num_examples = self.data_issues.get_info("statistics").get("num_examples") + num_classes = self.data_issues.get_info("statistics").get("num_classes") + + issues_identified = ( + self.data_issues.issue_summary["num_issues"].sum() if checks_run else "Not checked" + ) + info_list = [ + f"Checks run: {'Yes' if checks_run else 'No'}", + f"Number of examples: {num_examples if num_examples is not None else 'Unknown'}", + f"Number of classes: {num_classes if num_classes is not None else 'Unknown'}", + f"Issues identified: {issues_identified}", + ] + + return "Datalab:\n" + "\n".join(info_list) diff --git a/cleanlab/datalab/examples/__init__.py b/cleanlab/datalab/examples/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cleanlab/datalab/factory.py b/cleanlab/datalab/factory.py new file mode 100644 index 0000000000..0cbe8f5262 --- /dev/null +++ b/cleanlab/datalab/factory.py @@ -0,0 +1,155 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . +"""The factory module provides a factory class for constructing concrete issue managers +and a decorator for registering new issue managers. + +This module provides the :py:meth:`register` decorator for users to register new subclasses of +:py:class:`IssueManager ` +in the registry. Each IssueManager detects some particular type of issue in a dataset. + + +Note +---- + +The :class:`REGISTRY` variable is used by the factory class to keep track +of registered issue managers. +The factory class is used as an implementation detail by +:py:class:`Datalab `, +which provides a simplified API for constructing concrete issue managers. +:py:class:`Datalab ` is intended to be used by users +and provides detailed documentation on how to use the API. + +Warning +------- +Neither the :class:`REGISTRY` variable nor the factory class should be used directly by users. +""" +from __future__ import annotations + +from typing import Dict, List, Type + +from cleanlab.datalab.issue_manager import ( + IssueManager, + LabelIssueManager, + NearDuplicateIssueManager, + OutlierIssueManager, + NonIIDIssueManager, +) + + +REGISTRY: Dict[str, Type[IssueManager]] = { + "outlier": OutlierIssueManager, + "label": LabelIssueManager, + "near_duplicate": NearDuplicateIssueManager, + "non_iid": NonIIDIssueManager, +} +"""Registry of issue managers that can be constructed from a string +and used in the Datalab class. + +:meta hide-value: + +Currently, the following issue managers are registered by default: + +- ``"outlier"``: :py:class:`OutlierIssueManager ` +- ``"label"``: :py:class:`LabelIssueManager ` +- ``"near_duplicate"``: :py:class:`NearDuplicateIssueManager ` +- ``"non_iid"``: :py:class:`NonIIDIssueManager ` + +Warning +------- +This variable should not be used directly by users. +""" + + +# Construct concrete issue manager with a from_str method +class _IssueManagerFactory: + """Factory class for constructing concrete issue managers.""" + + @classmethod + def from_str(cls, issue_type: str) -> Type[IssueManager]: + """Constructs a concrete issue manager class from a string.""" + if isinstance(issue_type, list): + raise ValueError( + "issue_type must be a string, not a list. Try using from_list instead." + ) + if issue_type not in REGISTRY: + raise ValueError(f"Invalid issue type: {issue_type}") + return REGISTRY[issue_type] + + @classmethod + def from_list(cls, issue_types: List[str]) -> List[Type[IssueManager]]: + """Constructs a list of concrete issue manager classes from a list of strings.""" + return [cls.from_str(issue_type) for issue_type in issue_types] + + +def register(cls: Type[IssueManager]) -> Type[IssueManager]: + """Registers the issue manager factory. + + Parameters + ---------- + cls : + A subclass of + :py:class:`IssueManager `. + + Returns + ------- + cls : + The same class that was passed in. + + Example + ------- + + When defining a new subclass of + :py:class:`IssueManager `, + you can register it like so: + + .. code-block:: python + + from cleanlab import IssueManager + from cleanlab.datalab.factory import register + + @register + class MyIssueManager(IssueManager): + issue_name: str = "my_issue" + def find_issues(self, **kwargs): + # Some logic to find issues + pass + + or in a function call: + + .. code-block:: python + + from cleanlab import IssueManager + from cleanlab.datalab.factory import register + + class MyIssueManager(IssueManager): + issue_name: str = "my_issue" + def find_issues(self, **kwargs): + # Some logic to find issues + pass + + register(MyIssueManager) + """ + name: str = str(cls.issue_name) + if name in REGISTRY: + # Warn user that they are overwriting an existing issue manager + print( + f"Warning: Overwriting existing issue manager {name} with {cls}. " + "This may cause unexpected behavior." + ) + if not issubclass(cls, IssueManager): + raise ValueError(f"Class {cls} must be a subclass of IssueManager") + REGISTRY[name] = cls + return cls diff --git a/cleanlab/datalab/issue_finder.py b/cleanlab/datalab/issue_finder.py new file mode 100644 index 0000000000..8a91e7ea4b --- /dev/null +++ b/cleanlab/datalab/issue_finder.py @@ -0,0 +1,370 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . +""" +Module for the :class:`IssueFinder` class, which is responsible for configuring, +creating and running issue managers. + +It determines which types of issues to look for, instatiates the IssueManagers +via a factory, run the issue managers +(:py:meth:`IssueManager.find_issues `), +and collects the results to :py:class:`DataIssues `. + +.. note:: + + This module is not intended to be used directly. Instead, use the public-facing + :py:meth:`Datalab.find_issues ` method. +""" +from __future__ import annotations + +from typing import Any, List, Optional, Dict, TYPE_CHECKING +import warnings + +import numpy as np +from scipy.sparse import csr_matrix + +from cleanlab.datalab.factory import _IssueManagerFactory, REGISTRY + +if TYPE_CHECKING: # pragma: no cover + import numpy.typing as npt + from cleanlab.datalab.datalab import Datalab + + +class IssueFinder: + """ + The IssueFinder class is responsible for managing the process of identifying + issues in the dataset by handling the creation and execution of relevant + IssueManagers. It serves as a coordinator or helper class for the Datalab class + to encapsulate the specific behavior of the issue finding process. + + At a high level, the IssueFinder is responsible for: + + - Determining which types of issues to look for. + - Instantiating the appropriate IssueManagers using a factory. + - Running the IssueManagers' `find_issues` methods. + - Collecting the results into a DataIssues instance. + + Parameters + ---------- + datalab : Datalab + The Datalab instance associated with this IssueFinder. + + verbosity : int + Controls the verbosity of the output during the issue finding process. + + Note + ---- + This class is not intended to be used directly. Instead, use the + `Datalab.find_issues` method which internally utilizes an IssueFinder instance. + """ + + def __init__(self, datalab: "Datalab", verbosity=1): + self.datalab = datalab + self.verbosity = verbosity + + def find_issues( + self, + *, + pred_probs: Optional[np.ndarray] = None, + features: Optional[npt.NDArray] = None, + knn_graph: Optional[csr_matrix] = None, + issue_types: Optional[Dict[str, Any]] = None, + ) -> None: + """ + Checks the dataset for all sorts of common issues in real-world data (in both labels and feature values). + + You can use Datalab to find issues in your data, utilizing *any* model you have already trained. + This method only interacts with your model via its predictions or embeddings (and other functions thereof). + The more of these inputs you provide, the more types of issues Datalab can detect in your dataset/labels. + If you provide a subset of these inputs, Datalab will output what insights it can based on the limited information from your model. + + Note + ---- + This method is not intended to be used directly. Instead, use the + :py:meth:`Datalab.find_issues ` method. + + Note + ---- + The issues are saved in the ``self.datalab.data_issues.issues`` attribute, but are not returned. + + Parameters + ---------- + pred_probs : + Out-of-sample predicted class probabilities made by the model for every example in the dataset. + To best detect label issues, provide this input obtained from the most accurate model you can produce. + + If provided, this must be a 2D array with shape (num_examples, K) where K is the number of classes in the dataset. + + features : Optional[np.ndarray] + Feature embeddings (vector representations) of every example in the dataset. + + If provided, this must be a 2D array with shape (num_examples, num_features). + + knn_graph : + Sparse matrix representing distances between examples in the dataset in a k nearest neighbor graph. + + If provided, this must be a square CSR matrix with shape (num_examples, num_examples) and (k*num_examples) non-zero entries (k is the number of nearest neighbors considered for each example) + evenly distributed across the rows. + The non-zero entries must be the distances between the corresponding examples. Self-distances must be omitted + (i.e. the diagonal must be all zeros and the k nearest neighbors of each example must not include itself). + + For any duplicated examples i,j whose distance is 0, there should be an *explicit* zero stored in the matrix, i.e. ``knn_graph[i,j] = 0``. + + If both `knn_graph` and `features` are provided, the `knn_graph` will take precendence. + If `knn_graph` is not provided, it is constructed based on the provided `features`. + If neither `knn_graph` nor `features` are provided, certain issue types like (near) duplicates will not be considered. + + issue_types : + Collection specifying which types of issues to consider in audit and any non-default parameter settings to use. + If unspecified, a default set of issue types and recommended parameter settings is considered. + + This is a dictionary of dictionaries, where the keys are the issue types of interest + and the values are dictionaries of parameter values that control how each type of issue is detected (only for advanced users). + More specifically, the values are constructor keyword arguments passed to the corresponding ``IssueManager``, + which is responsible for detecting the particular issue type. + + .. seealso:: + :py:class:`IssueManager ` + """ + + if issue_types is not None and not issue_types: + warnings.warn( + "No issue types were specified. " "No issues will be found in the dataset." + ) + return None + + if issue_types is not None and not issue_types: + warnings.warn( + "No issue types were specified. " "No issues will be found in the dataset." + ) + return None + + issue_types_copy = self.get_available_issue_types( + pred_probs=pred_probs, + features=features, + knn_graph=knn_graph, + issue_types=issue_types, + ) + + new_issue_managers = [ + factory(datalab=self.datalab, **issue_types_copy.get(factory.issue_name, {})) + for factory in _IssueManagerFactory.from_list(list(issue_types_copy.keys())) + ] + + if not new_issue_managers: + no_args_passed = all(arg is None for arg in [pred_probs, features, knn_graph]) + if no_args_passed: + warnings.warn("No arguments were passed to find_issues.") + warnings.warn("No issue check performed.") + return None + + failed_managers = [] + data_issues = self.datalab.data_issues + for issue_manager, arg_dict in zip(new_issue_managers, issue_types_copy.values()): + try: + if self.verbosity: + print(f"Finding {issue_manager.issue_name} issues ...") + issue_manager.find_issues(**arg_dict) + data_issues.collect_statistics_from_issue_manager(issue_manager) + data_issues.collect_results_from_issue_manager(issue_manager) + except Exception as e: + print(f"Error in {issue_manager.issue_name}: {e}") + failed_managers.append(issue_manager) + + if self.verbosity: + print( + f"Audit complete. {data_issues.issue_summary['num_issues'].sum()} issues found in the dataset." + ) + if failed_managers: + print(f"Failed to check for these issue types: {failed_managers}") + + data_issues.set_health_score() + + def _resolve_required_args(self, pred_probs, features, knn_graph): + """Resolves the required arguments for each issue type. + + This is a helper function that filters out any issue manager + that does not have the required arguments. + + This does not consider custom hyperparameters for each issue type. + + + Parameters + ---------- + pred_probs : + Out-of-sample predicted probabilities made on the data. + + features : + Name of column containing precomputed embeddings. + + Returns + ------- + args_dict : + Dictionary of required arguments for each issue type, if available. + """ + args_dict = { + "label": {"pred_probs": pred_probs}, + "outlier": {"pred_probs": pred_probs, "features": features, "knn_graph": knn_graph}, + "near_duplicate": {"features": features, "knn_graph": knn_graph}, + "non_iid": {"features": features, "knn_graph": knn_graph}, + } + + args_dict = { + k: {k2: v2 for k2, v2 in v.items() if v2 is not None} for k, v in args_dict.items() if v + } + + # Prefer `knn_graph` over `features` if both are provided. + for v in args_dict.values(): + if "knn_graph" in v and "features" in v: + warnings.warn( + "Both `features` and `knn_graph` were provided. " + "Most issue managers will likely prefer using `knn_graph` " + "instead of `features` for efficiency." + ) + + args_dict = {k: v for k, v in args_dict.items() if v} + + return args_dict + + def _set_issue_types( + self, + issue_types: Optional[Dict[str, Any]], + required_defaults_dict: Dict[str, Any], + ) -> Dict[str, Any]: + """Set necessary configuration for each IssueManager in a dictionary. + + While each IssueManager defines default values for its arguments, + the Datalab class needs to organize the calls to each IssueManager + with different arguments, some of which may be user-provided. + + Parameters + ---------- + issue_types : + Dictionary of issue types and argument configuration for their respective IssueManagers. + If None, then the `required_defaults_dict` is used. + + required_defaults_dict : + Dictionary of default parameter configuration for each issue type. + + Returns + ------- + issue_types_copy : + Dictionary of issue types and their parameter configuration. + The input `issue_types` is copied and updated with the necessary default values. + """ + if issue_types is not None: + issue_types_copy = issue_types.copy() + self._check_missing_args(required_defaults_dict, issue_types_copy) + else: + issue_types_copy = required_defaults_dict.copy() + # Check that all required arguments are provided. + self._validate_issue_types_dict(issue_types_copy, required_defaults_dict) + + # Remove None values from argument list, rely on default values in IssueManager + for key, value in issue_types_copy.items(): + issue_types_copy[key] = {k: v for k, v in value.items() if v is not None} + return issue_types_copy + + @staticmethod + def _check_missing_args(required_defaults_dict, issue_types): + for key, issue_type_value in issue_types.items(): + missing_args = set(required_defaults_dict.get(key, {})) - set(issue_type_value.keys()) + # Impute missing arguments with default values. + missing_dict = { + missing_arg: required_defaults_dict[key][missing_arg] + for missing_arg in missing_args + } + issue_types[key].update(missing_dict) + + @staticmethod + def _validate_issue_types_dict( + issue_types: Dict[str, Any], required_defaults_dict: Dict[str, Any] + ) -> None: + missing_required_args_dict = {} + for issue_name, required_args in required_defaults_dict.items(): + if issue_name in issue_types: + missing_args = set(required_args.keys()) - set(issue_types[issue_name].keys()) + if missing_args: + missing_required_args_dict[issue_name] = missing_args + if any(missing_required_args_dict.values()): + error_message = "" + for issue_name, missing_required_args in missing_required_args_dict.items(): + error_message += f"Required argument {missing_required_args} for issue type {issue_name} was not provided.\n" + raise ValueError(error_message) + + @staticmethod + def list_possible_issue_types() -> List[str]: + """Returns a list of all registered issue types. + + Any issue type that is not in this list cannot be used in the :py:meth:`find_issues` method. + + See Also + -------- + :py:class:`REGISTRY ` : All available issue types and their corresponding issue managers can be found here. + """ + return list(REGISTRY.keys()) + + @staticmethod + def list_default_issue_types() -> List[str]: + """Returns a list of the issue types that are run by default + when :py:meth:`find_issues` is called without specifying `issue_types`. + + See Also + -------- + :py:class:`REGISTRY ` : All available issue types and their corresponding issue managers can be found here. + """ + return ["label", "outlier", "near_duplicate", "non_iid"] + + def get_available_issue_types(self, **kwargs): + """Returns a dictionary of issue types that can be used in :py:meth:`Datalab.find_issues + ` method.""" + + pred_probs = kwargs.get("pred_probs", None) + features = kwargs.get("features", None) + knn_graph = kwargs.get("knn_graph", None) + issue_types = kwargs.get("issue_types", None) + + # Determine which parameters are required for each issue type + required_args_per_issue_type = self._resolve_required_args(pred_probs, features, knn_graph) + + issue_types_copy = self._set_issue_types(issue_types, required_args_per_issue_type) + + if issue_types is None: + # Only run default issue types if no issue types are specified + issue_types_copy = { + issue: issue_types_copy[issue] + for issue in self.list_default_issue_types() + if issue in issue_types_copy + } + + drop_label_check = "label" in issue_types_copy and not self.datalab.has_labels + if drop_label_check: + warnings.warn("No labels were provided. " "The 'label' issue type will not be run.") + issue_types_copy.pop("label") + + outlier_check_needs_features = "outlier" in issue_types_copy and not self.datalab.has_labels + if outlier_check_needs_features: + no_features = features is None + no_knn_graph = knn_graph is None + pred_probs_given = issue_types_copy["outlier"].get("pred_probs", None) is not None + + only_pred_probs_given = pred_probs_given and no_features and no_knn_graph + if only_pred_probs_given: + warnings.warn( + "No labels were provided. " "The 'outlier' issue type will not be run." + ) + issue_types_copy.pop("outlier") + + return issue_types_copy diff --git a/cleanlab/datalab/issue_manager/__init__.py b/cleanlab/datalab/issue_manager/__init__.py new file mode 100644 index 0000000000..0352a6a03a --- /dev/null +++ b/cleanlab/datalab/issue_manager/__init__.py @@ -0,0 +1,5 @@ +from .issue_manager import IssueManager # isort:skip +from .duplicate import NearDuplicateIssueManager +from .label import LabelIssueManager +from .outlier import OutlierIssueManager +from .noniid import NonIIDIssueManager diff --git a/cleanlab/datalab/issue_manager/duplicate.py b/cleanlab/datalab/issue_manager/duplicate.py new file mode 100644 index 0000000000..6a8105d285 --- /dev/null +++ b/cleanlab/datalab/issue_manager/duplicate.py @@ -0,0 +1,222 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Optional, Union +import warnings + +import numpy as np +import pandas as pd +from scipy.sparse import csr_matrix +from sklearn.neighbors import NearestNeighbors +from sklearn.utils.validation import check_is_fitted + +from cleanlab.datalab.issue_manager import IssueManager + +if TYPE_CHECKING: # pragma: no cover + import numpy.typing as npt + from cleanlab.datalab.datalab import Datalab + + +class NearDuplicateIssueManager(IssueManager): + """Manages issues related to near-duplicate examples.""" + + description: ClassVar[ + str + ] = """A (near) duplicate issue refers to two or more examples in + a dataset that are extremely similar to each other, relative + to the rest of the dataset. The examples flagged with this issue + may be exactly duplicated, or lie atypically close together when + represented as vectors (i.e. feature embeddings). + """ + issue_name: ClassVar[str] = "near_duplicate" + verbosity_levels = { + 0: [], + 1: [], + 2: ["threshold"], + } + + def __init__( + self, + datalab: Datalab, + metric: Optional[str] = None, + threshold: float = 0.13, + k: int = 10, + **_, + ): + super().__init__(datalab) + self.metric = metric + self.threshold = self._set_threshold(threshold) + self.k = k + self.near_duplicate_sets: List[List[int]] = [] + + def find_issues( + self, + features: Optional[npt.NDArray] = None, + **kwargs, + ) -> None: + knn_graph = self._process_knn_graph_from_inputs(kwargs) + old_knn_metric = self.datalab.get_info("statistics").get("knn_metric") + metric_changes = self.metric and self.metric != old_knn_metric + + if knn_graph is None or metric_changes: + if features is None: + raise ValueError( + "If a knn_graph is not provided, features must be provided to fit a new knn." + ) + if self.metric is None: + self.metric = "cosine" if features.shape[1] > 3 else "euclidean" + knn = NearestNeighbors(n_neighbors=self.k, metric=self.metric) + + if self.metric and self.metric != knn.metric: + warnings.warn( + f"Metric {self.metric} does not match metric {knn.metric} used to fit knn. " + "Most likely an existing NearestNeighbors object was passed in, but a different " + "metric was specified." + ) + self.metric = knn.metric + + try: + check_is_fitted(knn) + except: + knn.fit(features) + + knn_graph = knn.kneighbors_graph(mode="distance") + N = knn_graph.shape[0] + nn_distances = knn_graph.data.reshape(N, -1)[:, 0] + scores = np.tanh(nn_distances) + is_issue_column = nn_distances < self.threshold * np.median(nn_distances) + + self.issues = pd.DataFrame( + { + f"is_{self.issue_name}_issue": is_issue_column, + self.issue_score_key: scores, + }, + ) + + self.near_duplicate_sets = self._neighbors_within_radius(knn_graph, self.threshold) + + self.summary = self.make_summary(score=scores.mean()) + self.info = self.collect_info(knn_graph=knn_graph) + + @staticmethod + def _neighbors_within_radius(knn_graph: csr_matrix, radius: float): + """Returns a list of lists of indices of near-duplicate examples. + + Each list of indices represents a set of near-duplicate examples. + + If the list is empty for a given example, then that example is not + a near-duplicate of any other example. + """ + + N = knn_graph.shape[0] + distances = knn_graph.data.reshape(N, -1) + # Create a mask for the threshold + mask = distances < radius + + # Update the indptr to reflect the new number of neighbors + indptr = np.zeros(knn_graph.indptr.shape, dtype=knn_graph.indptr.dtype) + indptr[1:] = np.cumsum(mask.sum(axis=1)) + + # Filter the knn_graph based on the threshold + indices = knn_graph.indices[mask.ravel()] + near_duplicate_sets = [indices[indptr[i] : indptr[i + 1]] for i in range(N)] + + return near_duplicate_sets + + def _process_knn_graph_from_inputs(self, kwargs: Dict[str, Any]) -> Union[csr_matrix, None]: + """Determine if a knn_graph is provided in the kwargs or if one is already stored in the associated Datalab instance.""" + knn_graph_kwargs: Optional[csr_matrix] = kwargs.get("knn_graph", None) + knn_graph_stats = self.datalab.get_info("statistics").get("weighted_knn_graph", None) + + knn_graph: Optional[csr_matrix] = None + if knn_graph_kwargs is not None: + knn_graph = knn_graph_kwargs + elif knn_graph_stats is not None: + knn_graph = knn_graph_stats + + if isinstance(knn_graph, csr_matrix) and kwargs.get("k", 0) > ( + knn_graph.nnz // knn_graph.shape[0] + ): + # If the provided knn graph is insufficient, then we need to recompute the knn graph + # with the provided features + knn_graph = None + return knn_graph + + def collect_info(self, knn_graph: csr_matrix) -> dict: + issues_dict = { + "average_near_duplicate_score": self.issues[self.issue_score_key].mean(), + "near_duplicate_sets": self.near_duplicate_sets, + } + + params_dict = { + "metric": self.metric, + "k": self.k, + "threshold": self.threshold, + } + + N = knn_graph.shape[0] + dists = knn_graph.data.reshape(N, -1)[:, 0] + nn_ids = knn_graph.indices.reshape(N, -1)[:, 0] + + knn_info_dict = { + "nearest_neighbor": nn_ids.tolist(), + "distance_to_nearest_neighbor": dists.tolist(), + } + + statistics_dict = self._build_statistics_dictionary(knn_graph=knn_graph) + + info_dict = { + **issues_dict, + **params_dict, + **knn_info_dict, + **statistics_dict, + } + return info_dict + + def _build_statistics_dictionary(self, knn_graph: csr_matrix) -> Dict[str, Dict[str, Any]]: + statistics_dict: Dict[str, Dict[str, Any]] = {"statistics": {}} + + # Add the knn graph as a statistic if necessary + graph_key = "weighted_knn_graph" + old_knn_graph = self.datalab.get_info("statistics").get(graph_key, None) + old_graph_exists = old_knn_graph is not None + prefer_new_graph = ( + not old_graph_exists + or knn_graph.nnz > old_knn_graph.nnz + or self.metric != self.datalab.get_info("statistics").get("knn_metric", None) + ) + if prefer_new_graph: + statistics_dict["statistics"][graph_key] = knn_graph + if self.metric is not None: + statistics_dict["statistics"]["knn_metric"] = self.metric + + return statistics_dict + + def _set_threshold( + self, + threshold: float, + ) -> float: + """Computes nearest-neighbors thresholding for near-duplicate detection.""" + if threshold < 0: + warnings.warn( + f"Computed threshold {threshold} is less than 0. " + "Setting threshold to 0." + "This may indicate that either the only a few examples are in the dataset, " + "or the data is heavily skewed." + ) + threshold = 0 + return threshold diff --git a/cleanlab/datalab/issue_manager/issue_manager.py b/cleanlab/datalab/issue_manager/issue_manager.py new file mode 100644 index 0000000000..96795075ec --- /dev/null +++ b/cleanlab/datalab/issue_manager/issue_manager.py @@ -0,0 +1,345 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . +from __future__ import annotations + +from abc import ABC, ABCMeta, abstractmethod +from itertools import chain +from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Optional, Set, Tuple, Type, TypeVar +import json + +import numpy as np +import pandas as pd + +if TYPE_CHECKING: # pragma: no cover + from cleanlab.datalab.datalab import Datalab + + +T = TypeVar("T", bound="IssueManager") +TM = TypeVar("TM", bound="IssueManagerMeta") + + +class IssueManagerMeta(ABCMeta): + """Metaclass for IssueManager that adds issue_score_key to the class. + + :meta private: + """ + + issue_name: ClassVar[str] + issue_score_key: ClassVar[str] + verbosity_levels: ClassVar[Dict[int, List[str]]] = { + 0: [], + 1: [], + 2: [], + 3: [], + } + + def __new__( + meta: Type[TM], + name: str, + bases: Tuple[Type[Any], ...], + class_dict: Dict[str, Any], + ) -> TM: # Classes that inherit from ABC don't need to be modified + if ABC in bases: + return super().__new__(meta, name, bases, class_dict) + + # Ensure that the verbosity levels don't have keys other than those in ["issue", "info"] + verbosity_levels = class_dict.get("verbosity_levels", meta.verbosity_levels) + for level, level_list in verbosity_levels.items(): + if not isinstance(level_list, list): + raise ValueError( + f"Verbosity levels must be lists. " + f"Got {level_list} in {name}.verbosity_levels" + ) + prohibited_keys = [key for key in level_list if not isinstance(key, str)] + if prohibited_keys: + raise ValueError( + f"Verbosity levels must be lists of strings. " + f"Got {prohibited_keys} in {name}.verbosity_levels[{level}]" + ) + + # Concrete classes need to have an issue_name attribute + if "issue_name" not in class_dict: + raise TypeError("IssueManagers need an issue_name class variable") + + # Add issue_score_key to class + class_dict["issue_score_key"] = f"{class_dict['issue_name']}_score" + return super().__new__(meta, name, bases, class_dict) + + +class IssueManager(ABC, metaclass=IssueManagerMeta): + """Base class for managing data issues of a particular type in a Datalab. + + For each example in a dataset, the IssueManager for a particular type of issue should compute: + - A numeric severity score between 0 and 1, + with values near 0 indicating severe instances of the issue. + - A boolean `is_issue` value, which is True + if we believe this example suffers from the issue in question. + `is_issue` may be determined by thresholding the severity score + (with an a priori determined reasonable threshold value), + or via some other means (e.g. Confident Learning for flagging label issues). + + The IssueManager should also report: + - A global value between 0 and 1 summarizing how severe this issue is in the dataset overall + (e.g. the average severity across all examples in dataset + or count of examples where `is_issue=True`). + - Other interesting `info` about the issue and examples in the dataset, + and statistics estimated from current dataset that may be reused + to score this issue in future data. + For example, `info` for label issues could contain the: + confident_thresholds, confident_joint, predicted label for each example, etc. + Another example is for (near)-duplicate detection issue, where `info` could contain: + which set of examples in the dataset are all (nearly) identical. + + Implementing a new IssueManager: + - Define the `issue_name` class attribute, e.g. "label", "duplicate", "outlier", etc. + - Implement the abstract methods `find_issues` and `collect_info`. + - `find_issues` is responsible for computing computing the `issues` and `summary` dataframes. + - `collect_info` is responsible for computing the `info` dict. It is called by `find_issues`, + once the manager has set the `issues` and `summary` dataframes as instance attributes. + """ + + description: ClassVar[str] = "" + """Short text that summarizes the type of issues handled by this IssueManager. + + :meta hide-value: + """ + issue_name: ClassVar[str] + """Returns a key that is used to store issue summary results about the assigned Lab.""" + issue_score_key: ClassVar[str] + """Returns a key that is used to store issue score results about the assigned Lab.""" + verbosity_levels: ClassVar[Dict[int, List[str]]] = { + 0: [], + 1: [], + 2: [], + 3: [], + } + """A dictionary of verbosity levels and their corresponding dictionaries of + report items to print. + + :meta hide-value: + + Example + ------- + + >>> verbosity_levels = { + ... 0: [], + ... 1: ["some_info_key"], + ... 2: ["additional_info_key"], + ... } + """ + + def __init__(self, datalab: Datalab, **_): + self.datalab = datalab + self.info: Dict[str, Any] = {} + self.issues: pd.DataFrame = pd.DataFrame() + self.summary: pd.DataFrame = pd.DataFrame() + + def __repr__(self): + class_name = self.__class__.__name__ + return class_name + + @classmethod + def __init_subclass__(cls): + required_class_variables = [ + "issue_name", + ] + for var in required_class_variables: + if not hasattr(cls, var): + raise NotImplementedError(f"Class {cls.__name__} must define class variable {var}") + + @abstractmethod + def find_issues(self, *args, **kwargs) -> None: + """Finds occurrences of this particular issue in the dataset. + + Computes the `issues` and `summary` dataframes. Calls `collect_info` to compute the `info` dict. + """ + raise NotImplementedError + + def collect_info(self, *args, **kwargs) -> dict: + """Collects data for the info attribute of the Datalab. + + NOTE + ---- + This method is called by :py:meth:`find_issues` after :py:meth:`find_issues` has set the `issues` and `summary` dataframes + as instance attributes. + """ + raise NotImplementedError + + @classmethod + def make_summary(cls, score: float) -> pd.DataFrame: + """Construct a summary dataframe. + + Parameters + ---------- + score : + The overall score for this issue. + + Returns + ------- + summary : + A summary dataframe. + """ + if not 0 <= score <= 1: + raise ValueError(f"Score must be between 0 and 1. Got {score}.") + + return pd.DataFrame( + { + "issue_type": [cls.issue_name], + "score": [score], + }, + ) + + @classmethod + def report( + cls, + issues: pd.DataFrame, + summary: pd.DataFrame, + info: Dict[str, Any], + num_examples: int = 5, + verbosity: int = 0, + include_description: bool = False, + info_to_omit: Optional[List[str]] = None, + ) -> str: + """Compose a report of the issues found by this IssueManager. + + Parameters + ---------- + issues : + An issues dataframe. + + Example + ------- + >>> import pandas as pd + >>> issues = pd.DataFrame( + ... { + ... "is_X_issue": [True, False, True], + ... "X_score": [0.2, 0.9, 0.4], + ... }, + ... ) + + summary : + The summary dataframe. + + Example + ------- + >>> summary = pd.DataFrame( + ... { + ... "issue_type": ["X"], + ... "score": [0.5], + ... }, + ... ) + + info : + The info dict. + + Example + ------- + >>> info = { + ... "A": "val_A", + ... "B": ["val_B1", "val_B2"], + ... } + + num_examples : + The number of examples to print. + + verbosity : + The verbosity level of the report. + + include_description : + Whether to include a description of the issue in the report. + + Returns + ------- + report_str : + A string containing the report. + """ + + max_verbosity = max(cls.verbosity_levels.keys()) + top_level = max_verbosity + 1 + if verbosity not in list(cls.verbosity_levels.keys()) + [top_level]: + raise ValueError( + f"Verbosity level {verbosity} not supported. " + f"Supported levels: {cls.verbosity_levels.keys()}" + f"Use verbosity={top_level} to print all info." + ) + if issues.empty: + print(f"No issues found") + + topk_ids = issues.sort_values(by=cls.issue_score_key, ascending=True).index[:num_examples] + + score = summary["score"].loc[0] + report_str = f"{' ' + cls.issue_name + ' issues ':-^60}\n\n" + + if include_description and cls.description: + description = cls.description + if verbosity == 0: + description = description.split("\n\n", maxsplit=1)[0] + report_str += "About this issue:\n\t" + description + "\n\n" + report_str += ( + f"Number of examples with this issue: {issues[f'is_{cls.issue_name}_issue'].sum()}\n" + f"Overall dataset quality in terms of this issue: {score:.4f}\n\n" + ) + + info_to_print: Set[str] = set() + _info_to_omit = set(issues.columns).union(info_to_omit or []) + verbosity_levels_values = chain.from_iterable( + list(cls.verbosity_levels.values())[: verbosity + 1] + ) + info_to_print.update(set(verbosity_levels_values) - _info_to_omit) + if verbosity == top_level: + info_to_print.update(set(info.keys()) - _info_to_omit) + + report_str += "Examples representing most severe instances of this issue:\n" + report_str += issues.loc[topk_ids].to_string() + + def truncate(s, max_len=4) -> str: + if hasattr(s, "shape") or hasattr(s, "ndim"): + s = np.array(s) + if s.ndim > 1: + description = f"array of shape {s.shape}\n" + with np.printoptions(threshold=max_len): + if s.ndim == 2: + description += f"{s}" + if s.ndim > 2: + description += f"{s}" + return description + s = s.tolist() + + if isinstance(s, list): + if all([isinstance(s_, list) for s_ in s]): + return truncate(np.array(s, dtype=object), max_len=max_len) + if len(s) > max_len: + s = s[:max_len] + ["..."] + return str(s) + + if info_to_print: + info_to_print_dict = {key: info[key] for key in info_to_print} + # Print the info dict, truncating arrays to 4 elements, + report_str += f"\n\nAdditional Information: " + for key, value in info_to_print_dict.items(): + if key == "statistics": + continue + if isinstance(value, dict): + report_str += f"\n{key}:\n{json.dumps(value, indent=4)}" + elif isinstance(value, pd.DataFrame): + max_rows = 5 + df_str = value.head(max_rows).to_string() + if len(value) > max_rows: + df_str += f"\n... (total {len(value)} rows)" + report_str += f"\n{key}:\n{df_str}" + else: + report_str += f"\n{key}: {truncate(value)}" + return report_str diff --git a/cleanlab/datalab/issue_manager/label.py b/cleanlab/datalab/issue_manager/label.py new file mode 100644 index 0000000000..e566080386 --- /dev/null +++ b/cleanlab/datalab/issue_manager/label.py @@ -0,0 +1,226 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional + +import numpy as np + +from cleanlab.classification import CleanLearning +from cleanlab.datalab.issue_manager import IssueManager +from cleanlab.internal.validation import assert_valid_inputs + +if TYPE_CHECKING: # pragma: no cover + import pandas as pd + + from cleanlab.datalab.datalab import Datalab + + +class LabelIssueManager(IssueManager): + """Manages label issues in a Datalab. + + Parameters + ---------- + datalab : + A Datalab instance. + + clean_learning_kwargs : + Keyword arguments to pass to the :py:meth:`CleanLearning ` constructor. + + health_summary_parameters : + Keyword arguments to pass to the :py:meth:`health_summary ` function. + """ + + description: ClassVar[ + str + ] = """Examples whose given label is estimated to be potentially incorrect + (e.g. due to annotation error) are flagged as having label issues. + """ + + issue_name: ClassVar[str] = "label" + verbosity_levels = { + 0: [], + 1: [], + 2: [], + 3: ["classes_by_label_quality", "overlapping_classes"], + } + + def __init__( + self, + datalab: Datalab, + clean_learning_kwargs: Optional[Dict[str, Any]] = None, + health_summary_parameters: Optional[Dict[str, Any]] = None, + **_, + ): + super().__init__(datalab) + self.cl = CleanLearning(**(clean_learning_kwargs or {})) + self.health_summary_parameters: Dict[str, Any] = ( + health_summary_parameters.copy() if health_summary_parameters else {} + ) + self._reset() + + @staticmethod + def _process_find_label_issues_kwargs(kwargs: Dict[str, Any]) -> Dict[str, Any]: + """Searches for keyword arguments that are meant for the + CleanLearning.find_label_issues method call + + Examples + -------- + >>> from cleanlab.datalab.issue_manager.label import LabelIssueManager + >>> LabelIssueManager._process_clean_learning_kwargs(thresholds=[0.1, 0.9]) + {'thresholds': [0.1, 0.9]} + """ + accepted_kwargs = [ + "thresholds", + "noise_matrix", + "inverse_noise_matrix", + "save_space", + "clf_kwargs", + "validation_func", + ] + return {k: v for k, v in kwargs.items() if k in accepted_kwargs and v is not None} + + def _reset(self) -> None: + """Reset the attributes of this manager based on the available datalab info + and the keyword arguments stored as instance attributes. + + This allows the builder to use pre-computed info from the datalab to speed up + some computations in the :py:meth:`find_issues` method. + """ + if not self.health_summary_parameters: + statistics_dict = self.datalab.get_info("statistics") + self.health_summary_parameters = { + "labels": self.datalab.labels, + "class_names": list(self.datalab._label_map.values()), + "num_examples": statistics_dict.get("num_examples"), + "joint": statistics_dict.get("joint", None), + "confident_joint": statistics_dict.get("confident_joint", None), + "multi_label": statistics_dict.get("multi_label", None), + "asymmetric": statistics_dict.get("asymmetric", None), + "verbose": False, + } + self.health_summary_parameters = { + k: v for k, v in self.health_summary_parameters.items() if v is not None + } + + def find_issues( + self, + pred_probs: np.ndarray, + **kwargs, + ) -> None: + self.health_summary_parameters.update({"pred_probs": pred_probs}) + # Find examples with label issues + self.issues = self.cl.find_label_issues( + labels=self.datalab.labels, + pred_probs=pred_probs, + **self._process_find_label_issues_kwargs(kwargs), + ) + self.issues.rename(columns={"label_quality": self.issue_score_key}, inplace=True) + + summary_dict = self.get_health_summary(pred_probs=pred_probs) + + # Get a summarized dataframe of the label issues + self.summary = self.make_summary(score=summary_dict["overall_label_health_score"]) + + # Collect info about the label issues + self.info = self.collect_info(issues=self.issues, summary_dict=summary_dict) + + # Drop columns from issues that are in the info + self.issues = self.issues.drop(columns=["given_label", "predicted_label"]) + + def get_health_summary(self, pred_probs) -> dict: + """Returns a short summary of the health of this Lab.""" + from cleanlab.dataset import health_summary + + # Validate input + self._validate_pred_probs(pred_probs) + + summary_kwargs = self._get_summary_parameters(pred_probs) + summary = health_summary(**summary_kwargs) + return summary + + def _get_summary_parameters(self, pred_probs) -> Dict["str", Any]: + """Collects a set of input parameters for the health summary function based on + any info available in the datalab. + + Parameters + ---------- + pred_probs : + The predicted probabilities for each example. + + kwargs : + Keyword arguments to pass to the health summary function. + + Returns + ------- + summary_parameters : + A dictionary of parameters to pass to the health summary function. + """ + if "confident_joint" in self.health_summary_parameters: + summary_parameters = { + "confident_joint": self.health_summary_parameters["confident_joint"] + } + elif all([x in self.health_summary_parameters for x in ["joint", "num_examples"]]): + summary_parameters = { + k: self.health_summary_parameters[k] for k in ["joint", "num_examples"] + } + else: + summary_parameters = { + "pred_probs": pred_probs, + "labels": self.datalab.labels, + } + + summary_parameters["class_names"] = self.health_summary_parameters["class_names"] + + for k in ["asymmetric", "verbose"]: + # Start with the health_summary_parameters, then override with kwargs + if k in self.health_summary_parameters: + summary_parameters[k] = self.health_summary_parameters[k] + + return ( + summary_parameters # will be called in `dataset.health_summary(**summary_parameters)` + ) + + def collect_info(self, issues: pd.DataFrame, summary_dict: dict) -> dict: + issues_info = { + "num_label_issues": sum(issues[f"is_{self.issue_name}_issue"]), + "average_label_quality": issues[self.issue_score_key].mean(), + "given_label": issues["given_label"].tolist(), + "predicted_label": issues["predicted_label"].tolist(), + } + + health_summary_info = { + "confident_joint": summary_dict["joint"], + "classes_by_label_quality": summary_dict["classes_by_label_quality"], + "overlapping_classes": summary_dict["overlapping_classes"], + } + + cl_info = {} + for k in self.cl.__dict__: + if k not in ["py", "noise_matrix", "inverse_noise_matrix", "confident_joint"]: + continue + cl_info[k] = self.cl.__dict__[k] + + info_dict = { + **issues_info, + **health_summary_info, + **cl_info, + } + + return info_dict + + def _validate_pred_probs(self, pred_probs) -> None: + assert_valid_inputs(X=None, y=self.datalab.labels, pred_probs=pred_probs) diff --git a/cleanlab/datalab/issue_manager/noniid.py b/cleanlab/datalab/issue_manager/noniid.py new file mode 100644 index 0000000000..658ab2ac17 --- /dev/null +++ b/cleanlab/datalab/issue_manager/noniid.py @@ -0,0 +1,440 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Union, cast +import warnings +import itertools + +from scipy.stats import gaussian_kde +import numpy as np +import pandas as pd +from scipy.sparse import csr_matrix +from sklearn.neighbors import NearestNeighbors +from sklearn.utils.validation import check_is_fitted + +from cleanlab.datalab.issue_manager import IssueManager + +if TYPE_CHECKING: # pragma: no cover + import numpy.typing as npt + from cleanlab.datalab.datalab import Datalab + + +def simplified_kolmogorov_smirnov_test( + neighbor_histogram: npt.NDArray[np.float64], + non_neighbor_histogram: npt.NDArray[np.float64], +) -> float: + """Computes the Kolmogorov-Smirnov statistic between two groups of data. + The statistic is the largest difference between the empirical cumulative + distribution functions (ECDFs) of the two groups. + + Parameters + ---------- + neighbor_histogram : + Histogram data for the nearest neighbor group. + + non_neighbor_histogram : + Histogram data for the non-neighbor group. + + Returns + ------- + statistic : + The KS statistic between the two ECDFs. + + Note + ---- + - Both input arrays should have the same length. + - The input arrays are histograms, which means they contain the count + or frequency of values in each group. The data in the histograms + should be normalized so that they sum to one. + + To calculate the KS statistic, the function first calculates the ECDFs + for both input arrays, which are step functions that show the cumulative + sum of the data up to each point. The function then calculates the + largest absolute difference between the two ECDFs. + """ + + neighbor_cdf = np.cumsum(neighbor_histogram) + non_neighbor_cdf = np.cumsum(non_neighbor_histogram) + + statistic = np.max(np.abs(neighbor_cdf - non_neighbor_cdf)) + return statistic + + +class NonIIDIssueManager(IssueManager): + """Manages issues related to non-iid data distributions. + + Parameters + ---------- + datalab : + The Datalab instance that this issue manager searches for issues in. + + metric : + The distance metric used to compute the KNN graph of the examples in the dataset. + If set to `None`, the metric will be automatically selected based on the dimensionality + of the features used to represent the examples in the dataset. + + k : + The number of nearest neighbors to consider when computing the KNN graph of the examples. + + num_permutations : + The number of trials to run when performing permutation testing to determine whether + the distribution of index-distances between neighbors in the dataset is IID or not. + + Note + ---- + This class will only flag a single example as an issue if the dataset is considered non-IID. This type of issue + is more relevant to the entire dataset as a whole, rather than to individual examples. + + """ + + description: ClassVar[ + str + ] = """Whether the dataset exhibits statistically significant + violations of the IID assumption like: + changepoints or shift, drift, autocorrelation, etc. + The specific violation considered is whether the + examples are ordered such that almost adjacent examples + tend to have more similar feature values. + """ + issue_name: ClassVar[str] = "non_iid" + verbosity_levels = { + 0: ["p-value"], + 1: [], + 2: [], + } + + def __init__( + self, + datalab: Datalab, + metric: Optional[str] = None, + k: int = 10, + num_permutations: int = 25, + seed: Optional[int] = 0, + significance_threshold: float = 0.05, + **_, + ): + super().__init__(datalab) + self.metric = metric + self.k = k + self.num_permutations = num_permutations + self.tests = { + "ks": simplified_kolmogorov_smirnov_test, + } + self.background_distribution = None + self.seed = seed + self.significance_threshold = significance_threshold + + def find_issues(self, features: Optional[npt.NDArray] = None, **kwargs) -> None: + knn_graph = self._process_knn_graph_from_inputs(kwargs) + old_knn_metric = self.datalab.get_info("statistics").get("knn_metric") + metric_changes = self.metric and self.metric != old_knn_metric + + knn = None # Won't be used if knn_graph is not None + + if knn_graph is None or metric_changes: + if features is None: + raise ValueError( + "If a knn_graph is not provided, features must be provided to fit a new knn." + ) + + if self.metric is None: + self.metric = "cosine" if features.shape[1] > 3 else "euclidean" + knn = NearestNeighbors(n_neighbors=self.k, metric=self.metric) + + if self.metric and self.metric != knn.metric: + warnings.warn( + f"Metric {self.metric} does not match metric {knn.metric} used to fit knn. " + "Most likely an existing NearestNeighbors object was passed in, but a different " + "metric was specified." + ) + self.metric = knn.metric + + try: + check_is_fitted(knn) + except: + knn.fit(features) + + self.neighbor_index_choices = self._get_neighbors(knn=knn) + else: + self.neighbor_index_choices = self._get_neighbors(knn_graph=knn_graph) + + self.num_neighbors = self.k + + indices = np.arange(self.N) + self.neighbor_index_distances = np.abs(indices.reshape(-1, 1) - self.neighbor_index_choices) + + self.statistics = self._get_statistics(self.neighbor_index_distances) + + self.p_value = self._permutation_test(num_permutations=self.num_permutations) + + scores = self._score_dataset() + issue_mask = np.zeros(self.N, dtype=bool) + if self.p_value < self.significance_threshold: + issue_mask[scores.argmin()] = True + self.issues = pd.DataFrame( + { + f"is_{self.issue_name}_issue": issue_mask, + self.issue_score_key: scores, + }, + ) + + self.summary = self.make_summary(score=self.p_value) + + if knn_graph is None: + self.info = self.collect_info(knn=knn) + self.info = self.collect_info(knn_graph=knn_graph, knn=knn) + + def _process_knn_graph_from_inputs(self, kwargs: Dict[str, Any]) -> Union[csr_matrix, None]: + """Determine if a knn_graph is provided in the kwargs or if one is already stored in the associated Datalab instance.""" + knn_graph_kwargs: Optional[csr_matrix] = kwargs.get("knn_graph", None) + knn_graph_stats = self.datalab.get_info("statistics").get("weighted_knn_graph", None) + + knn_graph: Optional[csr_matrix] = None + if knn_graph_kwargs is not None: + knn_graph = knn_graph_kwargs + elif knn_graph_stats is not None: + knn_graph = knn_graph_stats + + need_to_recompute_knn = isinstance(knn_graph, csr_matrix) and ( + kwargs.get("k", 0) > knn_graph.nnz // knn_graph.shape[0] + or self.k > knn_graph.nnz // knn_graph.shape[0] + ) + + if need_to_recompute_knn: + # If the provided knn graph is insufficient, then we need to recompute the knn graph + # with the provided features + knn_graph = None + return knn_graph + + def collect_info( + self, knn_graph: Optional[csr_matrix] = None, knn: Optional[NearestNeighbors] = None + ) -> dict: + issues_dict = { + "p-value": self.p_value, + } + + params_dict = { + "metric": self.metric, + "k": self.k, + } + if knn_graph is None: + assert knn is not None, "If knn_graph is None, knn must be provided." + knn_graph = knn.kneighbors_graph(mode="distance") # type: ignore[union-attr] + + assert knn_graph is not None, "knn_graph must be provided or computed." + statistics_dict = self._build_statistics_dictionary(knn_graph=knn_graph) + + info_dict = { + **issues_dict, + **params_dict, # type: ignore[arg-type] + **statistics_dict, # type: ignore[arg-type] + } + return info_dict + + def _build_statistics_dictionary(self, knn_graph: csr_matrix) -> Dict[str, Dict[str, Any]]: + statistics_dict: Dict[str, Dict[str, Any]] = {"statistics": {}} + + # Add the knn graph as a statistic if necessary + graph_key = "weighted_knn_graph" + old_knn_graph = self.datalab.get_info("statistics").get(graph_key, None) + old_graph_exists = old_knn_graph is not None + prefer_new_graph = ( + (knn_graph is not None and not old_graph_exists) + or knn_graph.nnz > old_knn_graph.nnz + or self.metric != self.datalab.get_info("statistics").get("knn_metric", None) + ) + if prefer_new_graph: + statistics_dict["statistics"][graph_key] = knn_graph + if self.metric is not None: + statistics_dict["statistics"]["knn_metric"] = self.metric + + return statistics_dict + + def _permutation_test(self, num_permutations) -> float: + N = self.N + + if self.seed is not None: + np.random.seed(self.seed) + perms = np.fromiter( + itertools.chain.from_iterable( + np.random.permutation(N) for i in range(num_permutations) + ), + dtype=int, + ).reshape(num_permutations, N) + + neighbor_index_choices = self.neighbor_index_choices + neighbor_index_choices = neighbor_index_choices.reshape(1, *neighbor_index_choices.shape) + perm_neighbor_choices = perms[:, neighbor_index_choices].reshape( + num_permutations, *neighbor_index_choices.shape[1:] + ) + neighbor_index_distances = np.abs(perms[..., None] - perm_neighbor_choices).reshape( + num_permutations, -1 + ) + + statistics = [] + for neighbor_index_dist in neighbor_index_distances: + stats = self._get_statistics( + neighbor_index_dist, + ) + statistics.append(stats) + + ks_stats = np.array([stats["ks"] for stats in statistics]) + ks_stats_kde = gaussian_kde(ks_stats) + p_value = ks_stats_kde.integrate_box(self.statistics["ks"], 100) + + return p_value + + def _score_dataset(self) -> npt.NDArray[np.float64]: + """This function computes a variant of the KS statistic for each + datapoint. Rather than computing the maximum difference + between the CDF of the neighbor distances (foreground + distribution) and the CDF of the all index distances + (background distribution), we compute the absolute difference + in area-under-the-curve of the two CDFs. + + The foreground distribution is computed by sampling the + neighbor distances from the KNN graph, but the background + distribution is computed analytically. The background CDF for + a datapoint i can be split up into three parts. Let d = min(i, + N - i - 1). + + 1. For 0 < j <= d, the slope of the CDF is 2 / (N - 1) since + there are two datapoints in the dataset that are distance j + from datapoint i. We call this threshold the 'double distance + threshold' + + 2. For d < j <= N - d - 1, the slope of the CDF is + 1 / (N - 1) since there is only one datapoint in the dataset + that is distance j from datapoint i. + + 3. For j > N - d - 1, the slope of the CDF is 0 and is + constant at 1.0 since there are no datapoints in the dataset + that are distance j from datapoint i. + + We compute the area differences on each of the k intervals for + which the foreground CDF is constant which allows for the + possibility that the background CDF may intersect the + foreground CDF on this interval. We do not account for these + cases when computing absolute AUC difference. + + Our algorithm is simple, sort the k sampled neighbor + distances. Then, for each of the k neighbor distances sampled, + compute the AUC for each CDF up to that point. Then, subtract + from each area the previous area in the sorted order to get + the AUC of the CDF on the interval between those two + points. Subtract the background interval AUCs from the + foreground interval AUCs, take the absolute value, and + sum. The algorithm is vectorized such that this statistic is + computed for each of the N datapoints simultaneously. + + The statistics are then normalized by their respective maximum + possible distance (N - d - 1) and then mapped to [0,1] via + tanh. + """ + N = self.N + + sorted_neighbors = np.sort(self.neighbor_index_distances, axis=1) + + # find the maximum distance that occurs with double probability + middle_idx = np.floor((N - 1) / 2).astype(int) + double_distances = np.arange(N).reshape(N, 1) + double_distances[double_distances > middle_idx] -= N - 1 + double_distances = np.abs(double_distances) + + sorted_neighbors = np.hstack([sorted_neighbors, np.ones((N, 1)) * (N - 1)]).astype(int) + + # the set of distances that are less than the double distance threshold + set_beginning = sorted_neighbors <= double_distances + # the set of distances that are greater than the double distance threshold but have nonzero probability + set_middle = (sorted_neighbors > double_distances) & ( + sorted_neighbors <= (N - double_distances - 1) + ) + # the set of distances that occur with 0 probability + set_end = sorted_neighbors > (N - double_distances - 1) + + shifted_neighbors = np.zeros(sorted_neighbors.shape) + shifted_neighbors[:, 1:] = sorted_neighbors[:, :-1] + diffs = sorted_neighbors - shifted_neighbors # the distances between the sorted indices + + area_beginning = (double_distances**2) / (N - 1) + length = N - 2 * double_distances - 1 + a = 2 * double_distances / (N - 1) + area_middle = 0.5 * (a + 1) * length + + # compute the area under the CDF for each of the indices in sorted_neighbors + background_area = np.zeros(diffs.shape) + background_diffs = np.zeros(diffs.shape) + background_area[set_beginning] = ((sorted_neighbors**2) / (N - 1))[set_beginning] + background_area[set_middle] = ( + area_beginning + + 0.5 + * ( + (sorted_neighbors + 3 * double_distances) + * (sorted_neighbors - double_distances) + / (N - 1) + ) + )[set_middle] + background_area[set_end] = ( + area_beginning + area_middle + (sorted_neighbors - (N - double_distances - 1) * 1.0) + )[set_end] + + # compute the area under the CDF between indices in sorted_neighbors + shifted_background = np.zeros(background_area.shape) + shifted_background[:, 1:] = background_area[:, :-1] + background_diffs = background_area - shifted_background + + # compute the foreground CDF and AUC between indices in sorted_neighbors + foreground_cdf = np.arange(sorted_neighbors.shape[1]) / (sorted_neighbors.shape[1] - 1) + foreground_diffs = foreground_cdf.reshape(1, -1) * diffs + + # compute the differences between foreground and background area intervals + area_diffs = np.abs(foreground_diffs - background_diffs) + stats = np.sum(area_diffs, axis=1) + + # normalize scores by the index and transform to [0, 1] + indices = np.arange(N) + reverse = N - indices + normalizer = np.where(indices > reverse, indices, reverse) + + scores = stats / normalizer + scores = np.tanh(-1 * scores) + 1 + return scores + + def _get_neighbors( + self, knn: Optional[NearestNeighbors] = None, knn_graph: Optional[csr_matrix] = None + ) -> np.ndarray: + """ + Given a fitted knn object or a knn graph, returns an (N, k) array in + which j is in A[i] if item i and j are nearest neighbors. + """ + if knn_graph is not None: + N = knn_graph.shape[0] + kneighbors = knn_graph.indices.reshape(N, -1) + elif knn is not None: + _, kneighbors = knn.kneighbors() + N = kneighbors.shape[0] + else: + raise ValueError("Must provide either knn or knn_graph") + self.N = N + return kneighbors + + def _get_statistics( + self, + neighbor_index_distances, + ) -> dict[str, float]: + neighbor_index_distances = neighbor_index_distances.flatten() + sorted_neighbors = np.sort(neighbor_index_distances) + sorted_neighbors = np.hstack([sorted_neighbors, np.ones((1)) * (self.N - 1)]).astype(int) + + if self.background_distribution is None: + self.background_distribution = (self.N - np.arange(1, self.N)) / ( + self.N * (self.N - 1) / 2 + ) + + background_distribution = cast(np.ndarray, self.background_distribution) + background_cdf = np.cumsum(background_distribution) + + foreground_cdf = np.arange(sorted_neighbors.shape[0]) / (sorted_neighbors.shape[0] - 1) + + statistic = np.max(np.abs(foreground_cdf - background_cdf[sorted_neighbors - 1])) + statistics = {"ks": statistic} + return statistics diff --git a/cleanlab/datalab/issue_manager/outlier.py b/cleanlab/datalab/issue_manager/outlier.py new file mode 100644 index 0000000000..68149c84f3 --- /dev/null +++ b/cleanlab/datalab/issue_manager/outlier.py @@ -0,0 +1,276 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Tuple, Union, cast + +from scipy.sparse import csr_matrix +from scipy.stats import iqr +import numpy as np +import pandas as pd + +from cleanlab.datalab.issue_manager import IssueManager +from cleanlab.outlier import OutOfDistribution, transform_distances_to_scores + +if TYPE_CHECKING: # pragma: no cover + import numpy.typing as npt + from sklearn.neighbors import NearestNeighbors + from cleanlab.datalab.datalab import Datalab + + +class OutlierIssueManager(IssueManager): + """Manages issues related to out-of-distribution examples.""" + + description: ClassVar[ + str + ] = """Examples that are very different from the rest of the dataset + (i.e. potentially out-of-distribution or rare/anomalous instances). + """ + issue_name: ClassVar[str] = "outlier" + verbosity_levels = { + 0: [], + 1: [], + 2: ["average_ood_score"], + 3: [], + } + + DEFAULT_THRESHOLDS = { + "features": 0.37037, + "pred_probs": 0.13, + } + """Default thresholds for outlier detection. + + If outlier detection is performed on the features, an example whose average + distance to their k nearest neighbors is greater than + Q3_avg_dist + (1 / threshold - 1) * IQR_avg_dist is considered an outlier. + + If outlier detection is performed on the predicted probabilities, an example + whose average score is lower than threshold * median_outlier_score is + considered an outlier. + """ + + def __init__( + self, + datalab: Datalab, + threshold: Optional[float] = None, + **kwargs, + ): + super().__init__(datalab) + + ood_kwargs = kwargs.get("ood_kwargs", {}) + + valid_ood_params = OutOfDistribution.DEFAULT_PARAM_DICT.keys() + params = { + key: value + for key, value in ((k, kwargs.get(k, None)) for k in valid_ood_params) + if value is not None + } + + if params: + ood_kwargs["params"] = params + + self.ood: OutOfDistribution = OutOfDistribution(**ood_kwargs) + + self.threshold = threshold + self._embeddings: Optional[np.ndarray] = None + self._metric: str = None # type: ignore + + def find_issues( + self, + features: Optional[npt.NDArray] = None, + pred_probs: Optional[np.ndarray] = None, + **kwargs, + ) -> None: + knn_graph = self._process_knn_graph_from_inputs(kwargs) + distances: Optional[np.ndarray] = None + + if knn_graph is not None: + N = knn_graph.shape[0] + k = knn_graph.nnz // N + t = cast(int, self.ood.params["t"]) + distances = knn_graph.data.reshape(-1, k) + assert isinstance(distances, np.ndarray) + scores = transform_distances_to_scores(distances, k=k, t=t) + elif features is not None: + scores = self._score_with_features(features, **kwargs) + elif pred_probs is not None: + scores = self._score_with_pred_probs(pred_probs, **kwargs) + else: + if kwargs.get("knn_graph", None) is not None: + raise ValueError( + "knn_graph is provided, but not sufficiently large to compute the scores based on the provided hyperparameters." + ) + raise ValueError(f"Either features pred_probs must be provided.") + + if features is not None or knn_graph is not None: + if knn_graph is None: + assert ( + features is not None + ), "features must be provided so that we can compute the knn graph." + knn_graph = self._process_knn_graph_from_features(kwargs) + distances = knn_graph.data.reshape(knn_graph.shape[0], -1) + + assert isinstance(distances, np.ndarray) + ( + self.threshold, + is_issue_column, + ) = self._compute_threshold_and_issue_column_from_distances(distances, self.threshold) + + else: + assert pred_probs is not None + # Threshold based on pred_probs, very small scores are outliers + if self.threshold is None: + self.threshold = self.DEFAULT_THRESHOLDS["pred_probs"] + if not 0 <= self.threshold: + raise ValueError(f"threshold must be non-negative, but got {self.threshold}.") + is_issue_column = scores < self.threshold * np.median(scores) + + self.issues = pd.DataFrame( + { + f"is_{self.issue_name}_issue": is_issue_column, + self.issue_score_key: scores, + }, + ) + + self.summary = self.make_summary(score=scores.mean()) + + self.info = self.collect_info(knn_graph=knn_graph) + + def _process_knn_graph_from_inputs(self, kwargs: Dict[str, Any]) -> Union[csr_matrix, None]: + """Determine if a knn_graph is provided in the kwargs or if one is already stored in the associated Datalab instance.""" + knn_graph_kwargs: Optional[csr_matrix] = kwargs.get("knn_graph", None) + knn_graph_stats = self.datalab.get_info("statistics").get("weighted_knn_graph", None) + + knn_graph: Optional[csr_matrix] = None + if knn_graph_kwargs is not None: + knn_graph = knn_graph_kwargs + elif knn_graph_stats is not None: + knn_graph = knn_graph_stats + + if isinstance(knn_graph, csr_matrix) and kwargs.get("k", 0) > ( + knn_graph.nnz // knn_graph.shape[0] + ): + # If the provided knn graph is insufficient, then we need to recompute the knn graph + # with the provided features + knn_graph = None + return knn_graph + + def _compute_threshold_and_issue_column_from_distances( + self, distances: np.ndarray, threshold: Optional[float] = None + ) -> Tuple[float, np.ndarray]: + avg_distances = distances.mean(axis=1) + if threshold: + if not (isinstance(threshold, (int, float)) and 0 <= threshold <= 1): + raise ValueError( + f"threshold must be a number between 0 and 1, got {threshold} of type {type(threshold)}." + ) + if threshold is None: + threshold = OutlierIssueManager.DEFAULT_THRESHOLDS["features"] + q3_distance = np.percentile(avg_distances, 75) + iqr_scale = 1 / threshold - 1 if threshold != 0 else np.inf + return threshold, avg_distances > q3_distance + iqr_scale * iqr(avg_distances) + + def _process_knn_graph_from_features(self, kwargs: Dict) -> csr_matrix: + # Check if the weighted knn graph exists in info + knn_graph = self.datalab.get_info("statistics").get("weighted_knn_graph", None) + + k: int = 0 # Used to check if the knn graph needs to be recomputed, already set in the knn object + if knn_graph is not None: + k = knn_graph.nnz // knn_graph.shape[0] + + knn: NearestNeighbors = self.ood.params["knn"] # type: ignore + if kwargs.get("knn", None) is not None or knn.n_neighbors > k: # type: ignore[union-attr] + # If the pre-existing knn graph has fewer neighbors than the knn object, + # then we need to recompute the knn graph + assert knn == self.ood.params["knn"] # type: ignore[union-attr] + knn_graph = knn.kneighbors_graph(mode="distance") # type: ignore[union-attr] + self._metric = knn.metric # type: ignore[union-attr] + + return knn_graph + + def collect_info(self, *, knn_graph: Optional[csr_matrix] = None) -> dict: + issues_dict = { + "average_ood_score": self.issues[self.issue_score_key].mean(), + "threshold": self.threshold, + } + pred_probs_issues_dict: Dict[str, Any] = {} + feature_issues_dict = {} + + if knn_graph is not None: + knn = self.ood.params["knn"] # type: ignore + N = knn_graph.shape[0] + k = knn_graph.nnz // N + dists = knn_graph.data.reshape(N, -1)[:, 0] + nn_ids = knn_graph.indices.reshape(N, -1)[:, 0] + + feature_issues_dict.update( + { + "k": k, # type: ignore[union-attr] + "nearest_neighbor": nn_ids.tolist(), + "distance_to_nearest_neighbor": dists.tolist(), + } + ) + if self.ood.params["knn"] is not None: + knn = self.ood.params["knn"] + feature_issues_dict.update({"metric": knn.metric}) # type: ignore[union-attr] + + if self.ood.params["confident_thresholds"] is not None: + pass # + statistics_dict = self._build_statistics_dictionary(knn_graph=knn_graph) + ood_params_dict = self.ood.params + knn_dict = { + **pred_probs_issues_dict, + **feature_issues_dict, + } + info_dict: Dict[str, Any] = { + **issues_dict, + **ood_params_dict, # type: ignore[arg-type] + **knn_dict, + **statistics_dict, + } + return info_dict + + def _build_statistics_dictionary( + self, *, knn_graph: Optional[csr_matrix] + ) -> Dict[str, Dict[str, Any]]: + statistics_dict: Dict[str, Dict[str, Any]] = {"statistics": {}} + + # Add the knn graph as a statistic if necessary + graph_key = "weighted_knn_graph" + old_knn_graph = self.datalab.get_info("statistics").get(graph_key, None) + old_graph_exists = old_knn_graph is not None + prefer_new_graph = ( + not old_graph_exists + or (isinstance(knn_graph, csr_matrix) and knn_graph.nnz > old_knn_graph.nnz) + or self._metric != self.datalab.get_info("statistics").get("knn_metric", None) + ) + if prefer_new_graph: + if knn_graph is not None: + statistics_dict["statistics"][graph_key] = knn_graph + if self._metric is not None: + statistics_dict["statistics"]["knn_metric"] = self._metric + + return statistics_dict + + def _score_with_pred_probs(self, pred_probs: np.ndarray, **kwargs) -> np.ndarray: + # Remove "threshold" from kwargs if it exists + kwargs.pop("threshold", None) + scores = self.ood.fit_score(pred_probs=pred_probs, labels=self.datalab.labels, **kwargs) + return scores + + def _score_with_features(self, features: npt.NDArray, **kwargs) -> npt.NDArray: + scores = self.ood.fit_score(features=features) + return scores diff --git a/cleanlab/datalab/report.py b/cleanlab/datalab/report.py new file mode 100644 index 0000000000..aec0d4e9ec --- /dev/null +++ b/cleanlab/datalab/report.py @@ -0,0 +1,144 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . +""" +Module that handles reporting of all types of issues identified in the data. +""" + +from typing import TYPE_CHECKING + +import pandas as pd + +from cleanlab.datalab.factory import _IssueManagerFactory + + +if TYPE_CHECKING: # pragma: no cover + from cleanlab.datalab.data_issues import DataIssues + + +class Reporter: + """Class that generates a report about the issues stored in a :py:class:`DataIssues` object. + + Parameters + ---------- + data_issues : + The :py:class:`DataIssues` object containing the issues to report on. This is usually + generated by the :py:class:`Datalab` class, stored in the :py:attr:`data_issues` attribute, + and then passed to the :py:class:`Reporter` class to generate a report. + + verbosity : + The default verbosity of the report to generate. Each :py:class`IssueManager` + specifies the available verbosity levels and what additional information + is included at each level. + + include_description : + Whether to include the description of each issue type in the report. The description + is included by default, but can be excluded by setting this parameter to ``False``. + + Note + ---- + This class is not intended to be used directly. Instead, use the + `Datalab.find_issues` method which internally utilizes an IssueFinder instance. + """ + + def __init__( + self, + data_issues: "DataIssues", + verbosity: int = 1, + include_description: bool = True, + show_summary_score: bool = False, + ): + self.data_issues = data_issues + self.verbosity = verbosity + self.include_description = include_description + self.show_summary_score = show_summary_score + + def report(self, num_examples: int) -> None: + """Prints a report about identified issues in the data. + + Parameters + ---------- + num_examples : + The number of examples to include in the report for each issue type. + """ + print(self.get_report(num_examples=num_examples)) + + def get_report(self, num_examples: int) -> str: + """Constructs a report about identified issues in the data. + + Parameters + ---------- + num_examples : + The number of examples to include in the report for each issue type. + + + Returns + ------- + report_str : + A string containing the report. + + Examples + -------- + >>> from cleanlab.datalab.report import Reporter + >>> reporter = Reporter(data_issues=data_issues, include_description=False) + >>> report_str = reporter.get_report(num_examples=5) + >>> print(report_str) + """ + report_str = "" + issue_summary = self.data_issues.issue_summary + issue_summary_sorted = issue_summary.sort_values(by="num_issues", ascending=False) + report_str += self._write_summary(summary=issue_summary_sorted) + + issue_reports = [ + _IssueManagerFactory.from_str(issue_type=key).report( + issues=self.data_issues.get_issues(issue_name=key), + summary=self.data_issues.get_issue_summary(issue_name=key), + info=self.data_issues.get_info(issue_name=key), + num_examples=num_examples, + verbosity=self.verbosity, + include_description=self.include_description, + ) + for key in issue_summary_sorted["issue_type"].tolist() + ] + + report_str += "\n\n\n".join(issue_reports) + return report_str + + def _write_summary(self, summary: pd.DataFrame) -> str: + statistics = self.data_issues.get_info("statistics") + num_examples = statistics["num_examples"] + num_classes = statistics.get( + "num_classes" + ) # This may not be required for all types of datasets in the future (e.g. unlabeled/regression) + + dataset_information = f"Dataset Information: num_examples: {num_examples}" + if num_classes is not None: + dataset_information += f", num_classes: {num_classes}" + + if self.show_summary_score: + return ( + "Here is a summary of the different kinds of issues found in the data:\n\n" + + summary.to_string(index=False) + + "\n\n" + + "(Note: A lower score indicates a more severe issue across all examples in the dataset.)\n\n" + + f"{dataset_information}\n\n\n" + ) + + return ( + "Here is a summary of the different kinds of issues found in the data:\n\n" + + summary.drop(columns=["score"]).to_string(index=False) + + "\n\n" + + f"{dataset_information}\n\n\n" + ) diff --git a/cleanlab/datalab/serialize.py b/cleanlab/datalab/serialize.py new file mode 100644 index 0000000000..548661f1a4 --- /dev/null +++ b/cleanlab/datalab/serialize.py @@ -0,0 +1,138 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . +from __future__ import annotations + +import os +import pickle +import warnings +from typing import TYPE_CHECKING, Optional + +import pandas as pd + +import cleanlab +from cleanlab.datalab.data import Data + +if TYPE_CHECKING: # pragma: no cover + from datasets.arrow_dataset import Dataset + + from cleanlab.datalab.datalab import Datalab + + +# Constants: +OBJECT_FILENAME = "datalab.pkl" +ISSUES_FILENAME = "issues.csv" +ISSUE_SUMMARY_FILENAME = "summary.csv" +INFO_FILENAME = "info.pkl" +DATA_DIRNAME = "data" + + +class _Serializer: + @staticmethod + def _save_data_issues(path: str, datalab: Datalab) -> None: + """Saves the issues to disk.""" + issues_path = os.path.join(path, ISSUES_FILENAME) + datalab.data_issues.issues.to_csv(issues_path, index=False) + + issue_summary_path = os.path.join(path, ISSUE_SUMMARY_FILENAME) + datalab.data_issues.issue_summary.to_csv(issue_summary_path, index=False) + + @staticmethod + def _save_data(path: str, datalab: Datalab) -> None: + """Saves the dataset to disk.""" + data_path = os.path.join(path, DATA_DIRNAME) + datalab.data.save_to_disk(data_path) + + @staticmethod + def _validate_version(datalab: Datalab) -> None: + current_version = cleanlab.__version__ # type: ignore[attr-defined] + datalab_version = datalab.cleanlab_version + if current_version != datalab_version: + warnings.warn( + f"Saved Datalab was created using different version of cleanlab " + f"({datalab_version}) than current version ({current_version}). " + f"Things may be broken!" + ) + + @classmethod + def serialize(cls, path: str, datalab: Datalab, force: bool) -> None: + """Serializes the datalab object to disk. + + Parameters + ---------- + path : str + Path to save the datalab object to. + + datalab : Datalab + The datalab object to save. + + force : bool + If True, will overwrite existing files at the specified path. + """ + path_exists = os.path.exists(path) + if not path_exists: + os.mkdir(path) + else: + if not force: + raise FileExistsError("Please specify a new path or set force=True") + print(f"WARNING: Existing files will be overwritten by newly saved files at: {path}") + + # Save the datalab object to disk. + with open(os.path.join(path, OBJECT_FILENAME), "wb") as f: + pickle.dump(datalab, f) + + # Save the issues to disk. Use placeholder method for now. + cls._save_data_issues(path=path, datalab=datalab) + + # Save the dataset to disk + cls._save_data(path=path, datalab=datalab) + + @classmethod + def deserialize(cls, path: str, data: Optional[Dataset] = None) -> Datalab: + """Deserializes the datalab object from disk.""" + + if not os.path.exists(path): + raise ValueError(f"No folder found at specified path: {path}") + + with open(os.path.join(path, OBJECT_FILENAME), "rb") as f: + datalab: Datalab = pickle.load(f) + + cls._validate_version(datalab) + + # Load the issues from disk. + issues_path = os.path.join(path, ISSUES_FILENAME) + if not hasattr(datalab.data_issues, "issues") and os.path.exists(issues_path): + datalab.data_issues.issues = pd.read_csv(issues_path) + + issue_summary_path = os.path.join(path, ISSUE_SUMMARY_FILENAME) + if not hasattr(datalab.data_issues, "issue_summary") and os.path.exists(issue_summary_path): + datalab.data_issues.issue_summary = pd.read_csv(issue_summary_path) + + if data is not None: + if hash(data) != hash(datalab._data): + raise ValueError( + "Data has been modified since Lab was saved. " + "Cannot load Lab with modified data." + ) + + if len(data) != len(datalab.labels): + raise ValueError( + f"Length of data ({len(data)}) does not match length of labels ({len(datalab.labels)})" + ) + + datalab._data = Data(data, datalab.label_name) + datalab.data = datalab._data._data + + return datalab diff --git a/cleanlab/dataset.py b/cleanlab/dataset.py index 40e2905a3a..d1f9a9afe9 100644 --- a/cleanlab/dataset.py +++ b/cleanlab/dataset.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -14,7 +14,6 @@ # You should have received a copy of the GNU Affero General Public License # along with cleanlab. If not, see . - """ Provides dataset-level and class-level overviews of issues in your classification dataset. If your task allows you to modify the classes in your dataset, this module can help you determine @@ -22,6 +21,7 @@ and which classes to merge (see :py:func:`find_overlapping_classes `). """ +from typing import Optional, cast import numpy as np import pandas as pd from cleanlab.count import estimate_joint @@ -53,6 +53,16 @@ def rank_classes_by_label_quality( Only provide **exactly one of the above input options**, do not provide a combination. + Examples + -------- + >>> from cleanlab.dataset import rank_classes_by_label_quality + >>> from sklearn.linear_model import LogisticRegression + >>> from sklearn.model_selection import cross_val_predict + >>> data, labels = get_data_labels_from_dataset() + >>> yourFavoriteModel = LogisticRegression() + >>> pred_probs = cross_val_predict(yourFavoriteModel, data, labels, cv=3, method="predict_proba") + >>> df = rank_classes_by_label_quality(labels=labels, pred_probs=pred_probs) + **Parameters**: For parameter info, see the docstring of :py:func:`find_overlapping_classes `. Returns @@ -75,13 +85,16 @@ def rank_classes_by_label_quality( By default, the DataFrame is ordered by "Label Quality Score", ascending. """ + if multi_label: + raise ValueError( + "For multilabel data, please instead call: multilabel_classification.dataset.overall_multilabel_health_score()" + ) if joint is None: joint = estimate_joint( labels=labels, pred_probs=pred_probs, confident_joint=confident_joint, - multi_label=multi_label, ) if num_examples is None: num_examples = _get_num_examples(labels=labels) @@ -138,6 +151,16 @@ def find_overlapping_classes( issues via the approach published in `Northcutt et al., 2021 `_. + Examples + -------- + >>> from cleanlab.dataset import find_overlapping_classes + >>> from sklearn.linear_model import LogisticRegression + >>> from sklearn.model_selection import cross_val_predict + >>> data, labels = get_data_labels_from_dataset() + >>> yourFavoriteModel = LogisticRegression() + >>> pred_probs = cross_val_predict(yourFavoriteModel, data, labels, cv=3, method="predict_proba") + >>> df = find_overlapping_classes(labels=labels, pred_probs=pred_probs) + Note ---- The joint distribution of noisy and true labels is asymmetric, and therefore the joint @@ -203,12 +226,6 @@ class 0, 1, ..., K-1. `pred_probs` should have been computed using 3 (or The `confident_joint` can be computed using :py:func:`count.compute_confident_joint `. If not provided, it is computed from the given (noisy) `labels` and `pred_probs`. - multi_label : bool, optional - If ``True``, labels should be an iterable (e.g. list) of iterables, containing a - list of labels for each example, instead of just a single label. - The multi-label setting supports classification tasks where an example has 1 or more labels. - Example of a multi-labeled `labels` input: ``[[0,1], [1], [0,2], [0,1,2], [0], [1], ...]``. - Returns ------- overlapping_classes : pd.DataFrame @@ -240,15 +257,19 @@ def _2d_matrix_to_row_column_value_list(matrix): return [(*i, v) for i, v in np.ndenumerate(matrix)] + if multi_label: + raise ValueError( + "For multilabel data, please instead call: multilabel_classification.dataset.common_multilabel_issues()" + ) + if joint is None: joint = estimate_joint( labels=labels, pred_probs=pred_probs, confident_joint=confident_joint, - multi_label=multi_label, ) if num_examples is None: - num_examples = _get_num_examples(labels=labels) + num_examples = _get_num_examples(labels=labels, confident_joint=confident_joint) if asymmetric: rcv_list = _2d_matrix_to_row_column_value_list(joint) # Remove diagonal elements @@ -296,21 +317,35 @@ def overall_label_health_score( Only provide **exactly one of the above input options**, do not provide a combination. + Examples + -------- + >>> from cleanlab.dataset import overall_label_health_score + >>> from sklearn.linear_model import LogisticRegression + >>> from sklearn.model_selection import cross_val_predict + >>> data, labels = get_data_labels_from_dataset() + >>> yourFavoriteModel = LogisticRegression() + >>> pred_probs = cross_val_predict(yourFavoriteModel, data, labels, cv=3, method="predict_proba") + >>> score = overall_label_health_score(labels=labels, pred_probs=pred_probs) # doctest: +SKIP + **Parameters**: For parameter info, see the docstring of :py:func:`find_overlapping_classes `. + Returns ------- health_score : float A score between 0 and 1, where 1 implies all labels in the dataset are estimated to be correct. A score of 0.5 implies that half of the dataset's labels are estimated to have issues. """ + if multi_label: + raise ValueError( + "For multilabel data, please instead call: multilabel_classification.dataset.overall_multilabel_health_score()" + ) if joint is None: joint = estimate_joint( labels=labels, pred_probs=pred_probs, confident_joint=confident_joint, - multi_label=multi_label, ) if num_examples is None: num_examples = _get_num_examples(labels=labels) @@ -337,11 +372,13 @@ def health_summary( multi_label=False, verbose=True, ) -> dict: - """Prints a health summary of your datasets including useful statistics like: + """Prints a health summary of your dataset. + + This summary includes useful statistics like: - * The classes with the most and least label issues - * Classes that overlap and could potentially be merged - * Overall data label quality health score statistics for your dataset + * The classes with the most and least label issues. + * Classes that overlap and could potentially be merged. + * Overall label quality scores, summarizing how accurate the labels appear overall. This method works by providing any one (and only one) of the following inputs: @@ -351,6 +388,16 @@ def health_summary( Only provide **exactly one of the above input options**, do not provide a combination. + Examples + -------- + >>> from cleanlab.dataset import health_summary + >>> from sklearn.linear_model import LogisticRegression + >>> from sklearn.model_selection import cross_val_predict + >>> data, labels = get_data_labels_from_dataset() + >>> yourFavoriteModel = LogisticRegression() + >>> pred_probs = cross_val_predict(yourFavoriteModel, data, labels, cv=3, method="predict_proba") + >>> summary = health_summary(labels=labels, pred_probs=pred_probs) # doctest: +SKIP + **Parameters**: For parameter info, see the docstring of :py:func:`find_overlapping_classes `. Returns @@ -365,12 +412,15 @@ def health_summary( """ from cleanlab.internal.util import smart_display_dataframe + if multi_label: + raise ValueError( + "For multilabel data, please call multilabel_classification.dataset.health_summary" + ) if joint is None: joint = estimate_joint( labels=labels, pred_probs=pred_probs, confident_joint=confident_joint, - multi_label=multi_label, ) if num_examples is None: num_examples = _get_num_examples(labels=labels) @@ -397,7 +447,6 @@ def health_summary( num_examples=num_examples, joint=joint, confident_joint=confident_joint, - multi_label=multi_label, ) if verbose: print("Overall Class Quality and Noise across your dataset (below)") @@ -412,7 +461,6 @@ def health_summary( num_examples=num_examples, joint=joint, confident_joint=confident_joint, - multi_label=multi_label, ) if verbose: print( @@ -431,7 +479,6 @@ def health_summary( num_examples=num_examples, joint=joint, confident_joint=confident_joint, - multi_label=multi_label, verbose=verbose, ) if verbose: @@ -444,13 +491,11 @@ def health_summary( } -def _get_num_examples(labels=None) -> int: +def _get_num_examples(labels=None, confident_joint: Optional[np.ndarray] = None) -> int: """Helper method that finds the number of examples from the parameters or throws an error if neither parameter is provided. - Parameters - ---------- - For parameter info, see the docstring of `dataset.find_overlapping_classes` + **Parameters:** For information about the arguments to this method, see the documentation of `dataset.find_overlapping_classes` Returns ------- @@ -462,11 +507,11 @@ def _get_num_examples(labels=None) -> int: ValueError If `labels` is None.""" - if labels is not None: - num_examples = len(labels) - else: + if labels is None and confident_joint is None: raise ValueError( - "Error: num_examples is None. You must provide a value for num_examples " - "when calling this method using the joint as an input parameter." + "Error: num_examples is None. You must either provide confident_joint, " + "or provide both num_example and joint as input parameters." ) + _confident_joint = cast(np.ndarray, confident_joint) + num_examples = len(labels) if labels is not None else cast(int, np.sum(_confident_joint)) return num_examples diff --git a/cleanlab/experimental/README.md b/cleanlab/experimental/README.md index 3c93ec9c71..3b24007b89 100644 --- a/cleanlab/experimental/README.md +++ b/cleanlab/experimental/README.md @@ -2,13 +2,9 @@ Methods in this `experimental` module are bleeding edge and may have sharp edges. They are not guaranteed to be stable between different cleanlab versions. -Some of these files include various models that can be used with cleanlab to find issues in specific types of data. These require dependencies on deep learning and other machine learning packages that are not official cleanlab dependencies. You must install these dependencies on your own if you wish to use them. +Some of these files include various models that can be used with cleanlab to find issues in specific types of data. These require dependencies on deep learning and other machine learning packages that are not official cleanlab dependencies. You must install these dependencies on your own if you wish to use them. -The dependencies are as follows: -* keras.py - a wrapper to make any Keras model compatible with cleanlab and sklearn - - tensorflow -* fasttext.py - a cleanlab-compatible FastText classifier for text data - - fasttext +The modules and required dependencies are as follows: * mnist_pytorch.py - a cleanlab-compatible simplified AlexNet for MNIST using PyTorch - torch - torchvision diff --git a/cleanlab/experimental/cifar_cnn.py b/cleanlab/experimental/cifar_cnn.py index 15e82d1b8e..13c08d35d3 100644 --- a/cleanlab/experimental/cifar_cnn.py +++ b/cleanlab/experimental/cifar_cnn.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify diff --git a/cleanlab/experimental/coteaching.py b/cleanlab/experimental/coteaching.py index b6247ee57d..83223ff523 100644 --- a/cleanlab/experimental/coteaching.py +++ b/cleanlab/experimental/coteaching.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -35,6 +35,7 @@ MINIMUM_BATCH_SIZE = 16 + # Loss function for Co-Teaching def loss_coteaching( y_1, diff --git a/cleanlab/experimental/label_issues_batched.py b/cleanlab/experimental/label_issues_batched.py new file mode 100644 index 0000000000..ef11f91b65 --- /dev/null +++ b/cleanlab/experimental/label_issues_batched.py @@ -0,0 +1,752 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . + +""" +Implementation of :py:func:`filter.find_label_issues ` +that does not need much memory by operating in mini-batches. +You can also use this approach to estimate label quality scores or the number of label issues +for big datasets with limited memory. + +With default settings, the results returned from this approach closely approximate those returned from: +``cleanlab.filter.find_label_issues(..., filter_by="low_self_confidence", return_indices_ranked_by="self_confidence")`` + +To run this approach, either use the ``find_label_issues_batched()`` convenience function defined in this module, +or follow the examples script for the ``LabelInspector`` class if you require greater customization. +""" + +import numpy as np +from typing import Optional, List, Tuple, Any + +from cleanlab.count import get_confident_thresholds +from cleanlab.rank import find_top_issues, _compute_label_quality_scores +from cleanlab.typing import LabelLike +from cleanlab.internal.util import value_counts_fill_missing_classes +from cleanlab.internal.constants import ( + CONFIDENT_THRESHOLDS_LOWER_BOUND, + FLOATING_POINT_COMPARISON, + CLIPPING_LOWER_BOUND, +) + +import platform +import multiprocessing as mp + +try: + import psutil + + PSUTIL_EXISTS = True +except ImportError: # pragma: no cover + PSUTIL_EXISTS = False + +# global variable for multiproc on linux +adj_confident_thresholds_shared: np.ndarray +labels_shared: LabelLike +pred_probs_shared: np.ndarray + + +def find_label_issues_batched( + labels: Optional[LabelLike] = None, + pred_probs: Optional[np.ndarray] = None, + *, + labels_file: Optional[str] = None, + pred_probs_file: Optional[str] = None, + batch_size: int = 10000, + n_jobs: Optional[int] = 1, + verbose: bool = True, + quality_score_kwargs: Optional[dict] = None, + num_issue_kwargs: Optional[dict] = None, +) -> np.ndarray: + """ + Variant of :py:func:`filter.find_label_issues ` + that requires less memory by reading from `pred_probs`, `labels` in mini-batches. + To avoid loading big `pred_probs`, `labels` arrays into memory, + provide these as memory-mapped objects like Zarr arrays or memmap arrays instead of regular numpy arrays. + See: https://pythonspeed.com/articles/mmap-vs-zarr-hdf5/ + + With default settings, the results returned from this method closely approximate those returned from: + ``cleanlab.filter.find_label_issues(..., filter_by="low_self_confidence", return_indices_ranked_by="self_confidence")`` + + This function internally implements the example usage script of the ``LabelInspector`` class, + but you can further customize that script by running it yourself instead of this function. + See the documentation of ``LabelInspector`` to learn more about how this method works internally. + + Parameters + ---------- + labels: np.ndarray-like object, optional + 1D array of given class labels for each example in the dataset, (int) values in ``0,1,2,...,K-1``. + To avoid loading big objects into memory, you should pass this as a memory-mapped object like: + Zarr array loaded with ``zarr.convenience.open(YOURFILE.zarr, mode="r")``, + or memmap array loaded with ``np.load(YOURFILE.npy, mmap_mode="r")``. + + Tip: You can save an existing numpy array to Zarr via: ``zarr.convenience.save_array(YOURFILE.zarr, your_array)``, + or to .npy file that can be loaded with mmap via: ``np.save(YOURFILE.npy, your_array)``. + + pred_probs: np.ndarray-like object, optional + 2D array of model-predicted class probabilities (floats) for each example in the dataset. + To avoid loading big objects into memory, you should pass this as a memory-mapped object like: + Zarr array loaded with ``zarr.convenience.open(YOURFILE.zarr, mode="r")`` + or memmap array loaded with ``np.load(YOURFILE.npy, mmap_mode="r")``. + + labels_file: str, optional + Specify this instead of `labels` if you want this method to load from file for you into a memmap array. + Path to .npy file where the entire 1D `labels` numpy array is stored on disk (list format is not supported). + This is loaded using: ``np.load(labels_file, mmap_mode="r")`` + so make sure this file was created via: ``np.save()`` or other compatible methods (.npz not supported). + + pred_probs_file: str, optional + Specify this instead of `pred_probs` if you want this method to load from file for you into a memmap array. + Path to .npy file where the entire `pred_probs` numpy array is stored on disk. + This is loaded using: ``np.load(pred_probs_file, mmap_mode="r")`` + so make sure this file was created via: ``np.save()`` or other compatible methods (.npz not supported). + + batch_size : int, optional + Size of mini-batches to use for estimating the label issues. + To maximize efficiency, try to use the largest `batch_size` your memory allows. + + n_jobs: int, optional + Number of processes for multiprocessing (default value = 1). Only used on Linux. + If `n_jobs=None`, will use either the number of: physical cores if psutil is installed, or logical cores otherwise. + + verbose : bool, optional + Whether to suppress print statements or not. + + quality_score_kwargs : dict, optional + Keyword arguments to pass into :py:func:`rank.get_label_quality_scores `. + + num_issue_kwargs : dict, optional + Keyword arguments to :py:func:`count.num_label_issues ` + to control estimation of the number of label issues. + The only supported kwarg here for now is: `estimation_method`. + + Returns + ------- + issue_indices : np.ndarray + Indices of examples with label issues, sorted by label quality score. + + Examples + -------- + >>> batch_size = 10000 # for efficiency, set this to as large of a value as your memory can handle + >>> # Just demonstrating how to save your existing numpy labels, pred_probs arrays to compatible .npy files: + >>> np.save("LABELS.npy", labels_array) + >>> np.save("PREDPROBS.npy", pred_probs_array) + >>> # You can load these back into memmap arrays via: labels = np.load("LABELS.npy", mmap_mode="r") + >>> # and then run this method on the memmap arrays, or just run it directly on the .npy files like this: + >>> issues = find_label_issues_batched(labels_file="LABELS.npy", pred_probs_file="PREDPROBS.npy", batch_size=batch_size) + >>> # This method also works with Zarr arrays: + >>> import zarr + >>> # Just demonstrating how to save your existing numpy labels, pred_probs arrays to compatible .zarr files: + >>> zarr.convenience.save_array("LABELS.zarr", labels_array) + >>> zarr.convenience.save_array("PREDPROBS.zarr", pred_probs_array) + >>> # You can load from such files into Zarr arrays: + >>> labels = zarr.convenience.open("LABELS.zarr", mode="r") + >>> pred_probs = zarr.convenience.open("PREDPROBS.zarr", mode="r") + >>> # This method can be directly run on Zarr arrays, memmap arrays, or regular numpy arrays: + >>> issues = find_label_issues_batched(labels=labels, pred_probs=pred_probs, batch_size=batch_size) + """ + if labels_file is not None: + if labels is not None: + raise ValueError("only specify one of: `labels` or `labels_file`") + if not isinstance(labels_file, str): + raise ValueError( + "labels_file must be str specifying path to .npy file containing the array of labels" + ) + labels = np.load(labels_file, mmap_mode="r") + assert isinstance(labels, np.ndarray) + + if pred_probs_file is not None: + if pred_probs is not None: + raise ValueError("only specify one of: `pred_probs` or `pred_probs_file`") + if not isinstance(pred_probs_file, str): + raise ValueError( + "pred_probs_file must be str specifying path to .npy file containing 2D array of pred_probs" + ) + pred_probs = np.load(pred_probs_file, mmap_mode="r") + assert isinstance(pred_probs, np.ndarray) + if verbose: + print( + f"mmap-loaded numpy arrays have: {len(pred_probs)} examples, {pred_probs.shape[1]} classes" + ) + if labels is None: + raise ValueError("must provide one of: `labels` or `labels_file`") + if pred_probs is None: + raise ValueError("must provide one of: `pred_probs` or `pred_probs_file`") + + assert pred_probs is not None + if len(labels) != len(pred_probs): + raise ValueError( + f"len(labels)={len(labels)} does not match len(pred_probs)={len(pred_probs)}. Perhaps an issue loading mmap numpy arrays from file." + ) + lab = LabelInspector( + num_class=pred_probs.shape[1], + verbose=verbose, + n_jobs=n_jobs, + quality_score_kwargs=quality_score_kwargs, + num_issue_kwargs=num_issue_kwargs, + ) + n = len(labels) + if verbose: + from tqdm.auto import tqdm + + pbar = tqdm(desc="number of examples processed for estimating thresholds", total=n) + i = 0 + while i < n: + end_index = i + batch_size + labels_batch = labels[i:end_index] + pred_probs_batch = pred_probs[i:end_index, :] + i = end_index + lab.update_confident_thresholds(labels_batch, pred_probs_batch) + if verbose: + pbar.update(batch_size) + + # Next evaluate the quality of the labels (run this on full dataset you want to evaluate): + if verbose: + pbar.close() + pbar = tqdm(desc="number of examples processed for checking labels", total=n) + i = 0 + while i < n: + end_index = i + batch_size + labels_batch = labels[i:end_index] + pred_probs_batch = pred_probs[i:end_index, :] + i = end_index + _ = lab.score_label_quality(labels_batch, pred_probs_batch) + if verbose: + pbar.update(batch_size) + + if verbose: + pbar.close() + + return lab.get_label_issues() + + +class LabelInspector: + """ + Class for finding label issues in big datasets where memory becomes a problem for other cleanlab methods. + Only create one such object per dataset and do not try to use the same ``LabelInspector`` across 2 datasets. + For efficiency, this class does little input checking. + You can first run :py:func:`filter.find_label_issues ` + on a small subset of your data to verify your inputs are properly formatted. + Do NOT modify any of the attributes of this class yourself! + Multi-label classification is not supported by this class, it is only for multi-class classification. + + The recommended usage demonstrated in the examples script below involves two passes over your data: + one pass to compute `confident_thresholds`, another to evaluate each label. + To maximize efficiency, try to use the largest batch_size your memory allows. + To reduce runtime further, you can run the first pass on a subset of your dataset + as long as it contains enough data from each class to estimate `confident_thresholds` accurately. + + In the examples script below: + - `labels` is a (big) 1D ``np.ndarray`` of class labels represented as integers in ``0,1,...,K-1``. + - ``pred_probs`` = is a (big) 2D ``np.ndarray`` of predicted class probabilities, + where each row is an example, each column represents a class. + + `labels` and `pred_probs` can be stored in a file instead where you load chunks of them at a time. + Methods to load arrays in chunks include: ``np.load(...,mmap_mode='r')``, ``numpy.memmap()``, + HDF5 or Zarr files, see: https://pythonspeed.com/articles/mmap-vs-zarr-hdf5/ + + Examples + -------- + >>> n = len(labels) + >>> batch_size = 10000 # you can change this in between batches, set as big as your RAM allows + >>> lab = LabelInspector(num_class = pred_probs.shape[1]) + >>> # First compute confident thresholds (for faster results, can also do this on a random subset of your data): + >>> i = 0 + >>> while i < n: + >>> end_index = i + batch_size + >>> labels_batch = labels[i:end_index] + >>> pred_probs_batch = pred_probs[i:end_index,:] + >>> i = end_index + >>> lab.update_confident_thresholds(labels_batch, pred_probs_batch) + >>> # See what we calculated: + >>> confident_thresholds = lab.get_confident_thresholds() + >>> # Evaluate the quality of the labels (run this on full dataset you want to evaluate): + >>> i = 0 + >>> while i < n: + >>> end_index = i + batch_size + >>> labels_batch = labels[i:end_index] + >>> pred_probs_batch = pred_probs[i:end_index,:] + >>> i = end_index + >>> batch_results = lab.score_label_quality(labels_batch, pred_probs_batch) + >>> # Indices of examples with label issues, sorted by label quality score (most severe to least severe): + >>> indices_of_examples_with_issues = lab.get_label_issues() + >>> # If your `pred_probs` and `labels` are arrays already in memory, + >>> # then you can use this shortcut for all of the above: + >>> indices_of_examples_with_issues = find_label_issues_batched(labels, pred_probs, batch_size=10000) + + Parameters + ---------- + num_class : int + The number of classes in your multi-class classification task. + + store_results : bool, optional + Whether this object will store all label quality scores, a 1D array of shape ``(N,)`` + where ``N`` is the total number of examples in your dataset. + Set this to False if you encounter memory problems even for small batch sizes (~1000). + If ``False``, you can still identify the label issues yourself by aggregating + the label quality scores for each batch, sorting them across all batches, and returning the top ``T`` indices + with ``T = self.get_num_issues()``. + + verbose : bool, optional + Whether to suppress print statements or not. + + n_jobs: int, optional + Number of processes for multiprocessing (default value = 1). Only used on Linux. + If `n_jobs=None`, will use either the number of: physical cores if psutil is installed, or logical cores otherwise. + + quality_score_kwargs : dict, optional + Keyword arguments to pass into :py:func:`rank.get_label_quality_scores `. + + num_issue_kwargs : dict, optional + Keyword arguments to :py:func:`count.num_label_issues ` + to control estimation of the number of label issues. + The only supported kwarg here for now is: `estimation_method`. + """ + + def __init__( + self, + *, + num_class: int, + store_results: bool = True, + verbose: bool = True, + quality_score_kwargs: Optional[dict] = None, + num_issue_kwargs: Optional[dict] = None, + n_jobs: Optional[int] = 1, + ): + if quality_score_kwargs is None: + quality_score_kwargs = {} + if num_issue_kwargs is None: + num_issue_kwargs = {} + + self.num_class = num_class + self.store_results = store_results + self.verbose = verbose + self.quality_score_kwargs = quality_score_kwargs # extra arguments for ``rank.get_label_quality_scores()`` to control label quality scoring + self.num_issue_kwargs = num_issue_kwargs # extra arguments for ``count.num_label_issues()`` to control estimation of the number of label issues (only supported argument for now is: `estimation_method`). + self.off_diagonal_calibrated = False + if num_issue_kwargs.get("estimation_method") == "off_diagonal_calibrated": + # store extra attributes later needed for calibration: + self.off_diagonal_calibrated = True + self.prune_counts = np.zeros(self.num_class) + self.class_counts = np.zeros(self.num_class) + self.normalization = np.zeros(self.num_class) + else: + self.prune_count = 0 # number of label issues estimated based on data seen so far (only used when estimation_method is not calibrated) + + if self.store_results: + self.label_quality_scores: List[float] = [] + + self.confident_thresholds = np.zeros( + (num_class,) + ) # current estimate of thresholds based on data seen so far + self.examples_per_class = np.zeros( + (num_class,) + ) # current counts of examples with each given label seen so far + self.examples_processed_thresh = ( + 0 # number of examples seen so far for estimating thresholds + ) + self.examples_processed_quality = 0 # number of examples seen so far for estimating label quality and number of label issues + # Determine number of cores for multiprocessing: + self.n_jobs: Optional[int] = None + os_name = platform.system() + if os_name != "Linux": + self.n_jobs = 1 + if n_jobs is not None and n_jobs != 1 and self.verbose: + print( + "n_jobs is overridden to 1 because multiprocessing is only supported for Linux." + ) + elif n_jobs is not None: + self.n_jobs = n_jobs + else: + if PSUTIL_EXISTS: + self.n_jobs = psutil.cpu_count(logical=False) # physical cores + if not self.n_jobs: + # switch to logical cores + self.n_jobs = mp.cpu_count() + if self.verbose: + print( + f"Multiprocessing will default to using the number of logical cores ({self.n_jobs}). To default to number of physical cores: pip install psutil" + ) + + def get_confident_thresholds(self, silent: bool = False) -> np.ndarray: + """ + Fetches already-computed confident thresholds from the data seen so far + in same format as: :py:func:`count.get_confident_thresholds `. + + + Returns + ------- + confident_thresholds : np.ndarray + An array of shape ``(K, )`` where ``K`` is the number of classes. + """ + if self.examples_processed_thresh < 1: + raise ValueError( + "Have not computed any confident_thresholds yet. Call `update_confident_thresholds()` first." + ) + else: + if self.verbose and not silent: + print( + f"Total number of examples used to estimate confident thresholds: {self.examples_processed_thresh}" + ) + return self.confident_thresholds + + def get_num_issues(self, silent: bool = False) -> int: + """ + Fetches already-computed estimate of the number of label issues in the data seen so far + in the same format as: :py:func:`count.num_label_issues `. + + Note: The estimated number of issues may differ from :py:func:`count.num_label_issues ` + by 1 due to rounding differences. + + Returns + ------- + num_issues : int + The estimated number of examples with label issues in the data seen so far. + """ + if self.examples_processed_quality < 1: + raise ValueError( + "Have not evaluated any labels yet. Call `score_label_quality()` first." + ) + else: + if self.verbose and not silent: + print( + f"Total number of examples whose labels have been evaluated: {self.examples_processed_quality}" + ) + if self.off_diagonal_calibrated: + calibrated_prune_counts = ( + self.prune_counts + * self.class_counts + / np.clip(self.normalization, a_min=CLIPPING_LOWER_BOUND, a_max=None) + ) # avoid division by 0 + return np.rint(np.sum(calibrated_prune_counts)).astype("int") + else: # not calibrated + return self.prune_count + + def get_quality_scores(self) -> np.ndarray: + """ + Fetches already-computed estimate of the label quality of each example seen so far + in the same format as: :py:func:`rank.get_label_quality_scores `. + + Returns + ------- + label_quality_scores : np.ndarray + Contains one score (between 0 and 1) per example seen so far. + Lower scores indicate more likely mislabeled examples. + """ + if not self.store_results: + raise ValueError( + "Must initialize the LabelInspector with `store_results` == True. " + "Otherwise you can assemble the label quality scores yourself based on " + "the scores returned for each batch of data from `score_label_quality()`" + ) + else: + return np.asarray(self.label_quality_scores) + + def get_label_issues(self) -> np.ndarray: + """ + Fetches already-computed estimate of indices of examples with label issues in the data seen so far, + in the same format as: :py:func:`filter.find_label_issues ` + with its `return_indices_ranked_by` argument specified. + + Note: this method corresponds to ``filter.find_label_issues(..., filter_by=METHOD1, return_indices_ranked_by=METHOD2)`` + where by default: ``METHOD1="low_self_confidence"``, ``METHOD2="self_confidence"`` + or if this object was instantiated with ``quality_score_kwargs = {"method": "normalized_margin"}`` then we instead have: + ``METHOD1="low_normalized_margin"``, ``METHOD2="normalized_margin"``. + + Note: The estimated number of issues may differ from :py:func:`filter.find_label_issues ` + by 1 due to rounding differences. + + Returns + ------- + issue_indices : np.ndarray + Indices of examples with label issues, sorted by label quality score. + """ + if not self.store_results: + raise ValueError( + "Must initialize the LabelInspector with `store_results` == True. " + "Otherwise you can identify label issues yourself based on the scores from all " + "the batches of data and the total number of issues returned by `get_num_issues()`" + ) + if self.examples_processed_quality < 1: + raise ValueError( + "Have not evaluated any labels yet. Call `score_label_quality()` first." + ) + if self.verbose: + print( + f"Total number of examples whose labels have been evaluated: {self.examples_processed_quality}" + ) + return find_top_issues(self.get_quality_scores(), top=self.get_num_issues(silent=True)) + + def update_confident_thresholds(self, labels: LabelLike, pred_probs: np.ndarray): + """ + Updates the estimate of confident_thresholds stored in this class using a new batch of data. + Inputs should be in same format as for: :py:func:`count.get_confident_thresholds `. + + Parameters + ---------- + labels: np.ndarray or list + Given class labels for each example in the batch, values in ``0,1,2,...,K-1``. + + pred_probs: np.ndarray + 2D array of model-predicted class probabilities for each example in the batch. + """ + labels = _batch_check(labels, pred_probs, self.num_class) + batch_size = len(labels) + batch_thresholds = get_confident_thresholds( + labels, pred_probs + ) # values for missing classes may exceed 1 but should not matter since we multiply by this class counts in the batch + batch_class_counts = value_counts_fill_missing_classes(labels, num_classes=self.num_class) + self.confident_thresholds = ( + self.examples_per_class * self.confident_thresholds + + batch_class_counts * batch_thresholds + ) / np.clip( + self.examples_per_class + batch_class_counts, a_min=1, a_max=None + ) # avoid division by 0 + self.confident_thresholds = np.clip( + self.confident_thresholds, a_min=CONFIDENT_THRESHOLDS_LOWER_BOUND, a_max=None + ) + self.examples_per_class += batch_class_counts + self.examples_processed_thresh += batch_size + + def score_label_quality( + self, + labels: LabelLike, + pred_probs: np.ndarray, + *, + update_num_issues: bool = True, + ) -> np.ndarray: + """ + Scores the label quality of each example in the provided batch of data, + and also updates the number of label issues stored in this class. + Inputs should be in same format as for: :py:func:`rank.get_label_quality_scores `. + + Parameters + ---------- + labels: np.ndarray + Given class labels for each example in the batch, values in ``0,1,2,...,K-1``. + + pred_probs: np.ndarray + 2D array of model-predicted class probabilities for each example in the batch of data. + + update_num_issues: bool, optional + Whether or not to update the number of label issues or only compute label quality scores. + For lower runtimes, set this to ``False`` if you only want to score label quality and not find label issues. + + Returns + ------- + label_quality_scores : np.ndarray + Contains one score (between 0 and 1) for each example in the batch of data. + """ + labels = _batch_check(labels, pred_probs, self.num_class) + batch_size = len(labels) + scores = _compute_label_quality_scores( + labels, + pred_probs, + confident_thresholds=self.get_confident_thresholds(silent=True), + **self.quality_score_kwargs, + ) + class_counts = value_counts_fill_missing_classes(labels, num_classes=self.num_class) + if update_num_issues: + self._update_num_label_issues(labels, pred_probs, **self.num_issue_kwargs) + self.examples_processed_quality += batch_size + if self.store_results: + self.label_quality_scores += list(scores) + + return scores + + def _update_num_label_issues( + self, + labels: LabelLike, + pred_probs: np.ndarray, + **kwargs, + ): + """ + Update the estimate of num_label_issues stored in this class using a new batch of data. + Kwargs are ignored here for now (included for forwards compatibility). + Instead of being specified here, `estimation_method` should be declared when this class is initialized. + """ + + # whether to match the output of count.num_label_issues exactly + # default is False, which gives significant speedup on large batches + # and empirically matches num_label_issues even on input sizes of + # 1M x 10k + thorough = False + if self.examples_processed_thresh < 1: + raise ValueError( + "Have not computed any confident_thresholds yet. Call `update_confident_thresholds()` first." + ) + + if self.n_jobs == 1: + adj_confident_thresholds = self.confident_thresholds - FLOATING_POINT_COMPARISON + pred_class = np.argmax(pred_probs, axis=1) + batch_size = len(labels) + if thorough: + # add margin for floating point comparison operations: + pred_gt_thresholds = pred_probs >= adj_confident_thresholds + max_ind = np.argmax(pred_probs * pred_gt_thresholds, axis=1) + if not self.off_diagonal_calibrated: + mask = (max_ind != labels) & (pred_class != labels) + else: + # calibrated + # should we change to above? + mask = pred_class != labels + else: + max_ind = pred_class + mask = pred_class != labels + + if not self.off_diagonal_calibrated: + prune_count_batch = np.sum( + ( + pred_probs[np.arange(batch_size), max_ind] + >= adj_confident_thresholds[max_ind] + ) + & mask + ) + self.prune_count += prune_count_batch + else: # calibrated + self.class_counts += value_counts_fill_missing_classes( + labels, num_classes=self.num_class + ) + to_increment = ( + pred_probs[np.arange(batch_size), max_ind] >= adj_confident_thresholds[max_ind] + ) + for class_label in range(self.num_class): + labels_equal_to_class = labels == class_label + self.normalization[class_label] += np.sum(labels_equal_to_class & to_increment) + self.prune_counts[class_label] += np.sum( + labels_equal_to_class + & to_increment + & (max_ind != labels) + # & (pred_class != labels) + # This is not applied in num_label_issues(..., estimation_method="off_diagonal_custom"). Do we want to add it? + ) + else: # multiprocessing implementation + global adj_confident_thresholds_shared + adj_confident_thresholds_shared = self.confident_thresholds - FLOATING_POINT_COMPARISON + + global labels_shared, pred_probs_shared + labels_shared = labels + pred_probs_shared = pred_probs + + # good values for this are ~1000-10000 in benchmarks where pred_probs has 1B entries: + processes = 5000 + if len(labels) <= processes: + chunksize = 1 + else: + chunksize = len(labels) // processes + inds = split_arr(np.arange(len(labels)), chunksize) + + if thorough: + use_thorough = np.ones(len(inds), dtype=bool) + else: + use_thorough = np.zeros(len(inds), dtype=bool) + args = zip(inds, use_thorough) + with mp.Pool(self.n_jobs) as pool: + if not self.off_diagonal_calibrated: + prune_count_batch = np.sum( + np.asarray(list(pool.imap_unordered(_compute_num_issues, args))) + ) + self.prune_count += prune_count_batch + else: + results = list(pool.imap_unordered(_compute_num_issues_calibrated, args)) + for result in results: + class_label = result[0] + self.class_counts[class_label] += 1 + self.normalization[class_label] += result[1] + self.prune_counts[class_label] += result[2] + + +def split_arr(arr: np.ndarray, chunksize: int) -> List[np.ndarray]: + """ + Helper function to split array into chunks for multiprocessing. + """ + return np.split(arr, np.arange(chunksize, arr.shape[0], chunksize), axis=0) + + +def _compute_num_issues(arg: Tuple[np.ndarray, bool]) -> int: + """ + Helper function for `_update_num_label_issues` multiprocessing without calibration. + """ + ind = arg[0] + thorough = arg[1] + label = labels_shared[ind] + pred_prob = pred_probs_shared[ind, :] + pred_class = np.argmax(pred_prob, axis=-1) + batch_size = len(label) + if thorough: + pred_gt_thresholds = pred_prob >= adj_confident_thresholds_shared + max_ind = np.argmax(pred_prob * pred_gt_thresholds, axis=-1) + prune_count_batch = np.sum( + (pred_prob[np.arange(batch_size), max_ind] >= adj_confident_thresholds_shared[max_ind]) + & (max_ind != label) + & (pred_class != label) + ) + else: + prune_count_batch = np.sum( + ( + pred_prob[np.arange(batch_size), pred_class] + >= adj_confident_thresholds_shared[pred_class] + ) + & (pred_class != label) + ) + return prune_count_batch + + +def _compute_num_issues_calibrated(arg: Tuple[np.ndarray, bool]) -> Tuple[Any, int, int]: + """ + Helper function for `_update_num_label_issues` multiprocessing with calibration. + """ + ind = arg[0] + thorough = arg[1] + label = labels_shared[ind] + pred_prob = pred_probs_shared[ind, :] + batch_size = len(label) + + pred_class = np.argmax(pred_prob, axis=-1) + if thorough: + pred_gt_thresholds = pred_prob >= adj_confident_thresholds_shared + max_ind = np.argmax(pred_prob * pred_gt_thresholds, axis=-1) + to_inc = ( + pred_prob[np.arange(batch_size), max_ind] >= adj_confident_thresholds_shared[max_ind] + ) + + prune_count_batch = to_inc & (max_ind != label) + normalization_batch = to_inc + else: + to_inc = ( + pred_prob[np.arange(batch_size), pred_class] + >= adj_confident_thresholds_shared[pred_class] + ) + normalization_batch = to_inc + prune_count_batch = to_inc & (pred_class != label) + + return (label, normalization_batch, prune_count_batch) + + +def _batch_check(labels: LabelLike, pred_probs: np.ndarray, num_class: int) -> np.ndarray: + """ + Basic checks to ensure batch of data looks ok. For efficiency, this check is quite minimal. + + Returns + ------- + labels : np.ndarray + `labels` formatted as a 1D array. + """ + batch_size = pred_probs.shape[0] + labels = np.asarray(labels) + if len(labels) != batch_size: + raise ValueError("labels and pred_probs must have same length") + if pred_probs.shape[1] != num_class: + raise ValueError("num_class must equal pred_probs.shape[1]") + + return labels diff --git a/cleanlab/experimental/mnist_pytorch.py b/cleanlab/experimental/mnist_pytorch.py index bdc9b6ffba..180f628f82 100644 --- a/cleanlab/experimental/mnist_pytorch.py +++ b/cleanlab/experimental/mnist_pytorch.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -311,7 +311,6 @@ def fit(self, train_idx, train_labels=None, sample_weight=None, loader="train"): # Train for self.epochs epochs for epoch in range(1, self.epochs + 1): - # Enable dropout and batch norm layers self.model.train() for batch_idx, (data, target) in enumerate(train_loader): diff --git a/cleanlab/filter.py b/cleanlab/filter.py index 7a65da6622..70b2d8d5da 100644 --- a/cleanlab/filter.py +++ b/cleanlab/filter.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -17,25 +17,22 @@ """ Methods to identify which examples have label issues in a classification dataset. The documentation below assumes a dataset with ``N`` examples and ``K`` classes. -This module considers two types of datasets: - -* standard (multi-class) classification where each example is labeled as belonging to exactly one of K classes (e.g. ``labels = np.array([0,0,1,0,2,1])``) -* multi-label classification where each example can be labeled as belonging to multiple classes (e.g. ``labels = [[1,2],[1],[0],[],...]``) +This module is for standard (multi-class) classification where each example is labeled as belonging to exactly one of K classes (e.g. ``labels = np.array([0,0,1,0,2,1])``). +Some methods here also work for multi-label classification data where each example can be labeled as belonging to multiple classes (e.g. ``labels = [[1,2],[1],[0],[],...]``), +but we encourage using the methods in the ``cleanlab.multilabel_classification`` module instead for such data. """ import numpy as np from sklearn.metrics import confusion_matrix import multiprocessing -from multiprocessing.sharedctypes import RawArray import sys import warnings -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, Optional, Tuple, List from functools import reduce +import platform -from cleanlab.count import calibrate_confident_joint -from cleanlab.rank import ( - order_label_issues, -) +from cleanlab.count import calibrate_confident_joint, num_label_issues +from cleanlab.rank import order_label_issues, get_label_quality_scores import cleanlab.internal.multilabel_scorer as ml_scorer from cleanlab.internal.validation import assert_valid_inputs from cleanlab.internal.util import ( @@ -45,10 +42,10 @@ ) from cleanlab.internal.multilabel_utils import stack_complement, get_onehot_num_classes, int2onehot from cleanlab.typing import LabelLike +from cleanlab.multilabel_classification.filter import find_multilabel_issues_per_class -# tqdm is a module used to print time-to-complete when multiprocessing is used. -# This module is not necessary, and therefore is not a package dependency, but -# when installed it improves user experience for large datasets. +# tqdm is a package to print time-to-complete when multiprocessing is used. +# This package is not necessary, but when installed improves user experience for large datasets. try: import tqdm @@ -59,6 +56,19 @@ w = """To see estimated completion times for methods in cleanlab.filter, "pip install tqdm".""" warnings.warn(w) +# psutil is a package used to count physical cores for multiprocessing +# This package is not necessary, because we can always fall back to logical cores as the default +try: + import psutil + + psutil_exists = True +except ImportError as e: # pragma: no cover + psutil_exists = False + +# global variable for find_label_issues multiprocessing +pred_probs_by_class: Dict[int, np.ndarray] +prune_count_matrix_cols: Dict[int, np.ndarray] + def find_label_issues( labels: LabelLike, @@ -67,13 +77,13 @@ def find_label_issues( return_indices_ranked_by: Optional[str] = None, rank_by_kwargs: Optional[Dict[str, Any]] = None, filter_by: str = "prune_by_noise_rate", - multi_label: bool = False, frac_noise: float = 1.0, - num_to_remove_per_class: Optional[int] = None, + num_to_remove_per_class: Optional[List[int]] = None, min_examples_per_class=1, confident_joint: Optional[np.ndarray] = None, n_jobs: Optional[int] = None, verbose: bool = False, + multi_label: bool = False, ) -> np.ndarray: """ Identifies potentially bad labels in a classification dataset using confident learning. @@ -99,8 +109,6 @@ def find_label_issues( *Format requirements*: for dataset with K classes, each label must be integer in 0, 1, ..., K-1. For a standard (multi-class) classification dataset where each example is labeled with one class, `labels` should be 1D array of shape ``(N,)``, for example: ``labels = [1,0,2,1,1,0...]``. - For a multi-label classification dataset where each example can belong to multiple (or no) classes, - `labels` should be an iterable of iterables (e.g. ``List[List[int]]``) whose i-th element corresponds to list of classes that i-th example belongs to (e.g. ``labels = [[1,2],[1],[0],[],...]``). pred_probs : np.ndarray, optional An array of shape ``(N, K)`` of model-predicted class probabilities, @@ -129,7 +137,7 @@ class 0, 1, ..., K-1. label quality score (see :py:func:`rank.get_label_quality_scores `). - filter_by : {'prune_by_class', 'prune_by_noise_rate', 'both', 'confident_learning', 'predicted_neq_given'}, default='prune_by_noise_rate' + filter_by : {'prune_by_class', 'prune_by_noise_rate', 'both', 'confident_learning', 'predicted_neq_given', 'low_normalized_margin', 'low_self_confidence'}, default='prune_by_noise_rate' Method to determine which examples are flagged as having label issue, so you can filter/prune them from the dataset. Options: - ``'prune_by_noise_rate'``: filters examples with *high probability* of being mislabeled for every non-diagonal in the confident joint (see `prune_counts_matrix` in `filter.py`). These are the examples where (with high confidence) the given label is unlikely to match the predicted label for the example. @@ -137,13 +145,8 @@ class 0, 1, ..., K-1. - ``'both'``: filters only those examples that would be filtered by both ``'prune_by_noise_rate'`` and ``'prune_by_class'``. - ``'confident_learning'``: filters the examples counted as part of the off-diagonals of the confident joint. These are the examples that are confidently predicted to be a different label than their given label. - ``'predicted_neq_given'``: filters examples for which the predicted class (i.e. argmax of the predicted probabilities) does not match the given label. - - multi_label : bool, optional - If ``True``, labels should be an iterable (e.g. list) of iterables, containing a - list of class labels for each example, instead of just a single label. - The multi-label setting supports classification tasks where an example can belong to more than 1 class or none of the classes (rather than exactly one class as in standard multi-class classification). - Example of a multi-labeled `labels` input: ``[[0,1], [1], [0,2], [0,1,2], [0], [1], [], ...]``. This says the first example in dataset belongs to both class 0 and class 1, according to its given label. - Each row of `pred_probs` no longer needs to sum to 1 in multi-label settings, since one example can now belong to multiple classes simultaneously. + - ``'low_normalized_margin'``: filters the examples with *smallest* normalized margin label quality score. The number of issues returned matches :py:func:`count.num_label_issues `. + - ``'low_self_confidence'``: filters the examples with *smallest* self confidence label quality score. The number of issues returned matches :py:func:`count.num_label_issues `. frac_noise : float, default=1.0 Used to only return the "top" ``frac_noise * num_label_issues``. The choice of which "top" @@ -155,7 +158,6 @@ class 0, 1, ..., K-1. When ``frac_noise=1.0``, return all "confident" estimated noise indices (recommended). frac_noise * number_of_mislabeled_examples_in_class_k. - Note: specifying `frac_noise` is not yet supported if `multi_label` is True. num_to_remove_per_class : array_like An iterable of length K, the number of classes. @@ -183,11 +185,10 @@ class 0, 1, ..., K-1. Entry ``(j, k)`` in the matrix is the number of examples confidently counted into the pair of ``(noisy label=j, true label=k)`` classes. The `confident_joint` can be computed using :py:func:`count.compute_confident_joint `. If not provided, it is computed from the given (noisy) `labels` and `pred_probs`. - If `multi_label` is True, `confident_joint` should instead be a one-vs-rest array with shape ``(K, 2, 2)`` as returned by :py:func:`count.compute_confident_joint ` function. n_jobs : optional Number of processing threads used by multiprocessing. Default ``None`` - sets to the number of cores on your CPU. + sets to the number of cores on your CPU (physical cores if you have ``psutil`` package installed, otherwise logical cores). Set this to 1 to *disable* parallel processing (if its causing issues). Windows users may see a speed-up with ``n_jobs=1``. @@ -211,6 +212,8 @@ class 0, 1, ..., K-1. rank_by_kwargs = {} assert filter_by in [ + "low_normalized_margin", + "low_self_confidence", "prune_by_noise_rate", "prune_by_class", "both", @@ -229,34 +232,79 @@ class 0, 1, ..., K-1. allow_one_class=allow_one_class, ) - if filter_by in ["confident_learning", "predicted_neq_given"] and ( - frac_noise != 1.0 or num_to_remove_per_class is not None - ): + if filter_by in [ + "confident_learning", + "predicted_neq_given", + "low_normalized_margin", + "low_self_confidence", + ] and (frac_noise != 1.0 or num_to_remove_per_class is not None): warn_str = ( - "WARNING! frac_noise and num_to_remove_per_class parameters are only supported" + "frac_noise and num_to_remove_per_class parameters are only supported" " for filter_by 'prune_by_noise_rate', 'prune_by_class', and 'both'. They " - "are not supported for methods 'confident_learning' or " - "'predicted_neq_given'." + "are not supported for methods 'confident_learning', 'predicted_neq_given', " + "'low_normalized_margin' or 'low_self_confidence'." ) warnings.warn(warn_str) if (num_to_remove_per_class is not None) and ( - filter_by in ["confident_learning", "predicted_neq_given"] + filter_by + in [ + "confident_learning", + "predicted_neq_given", + "low_normalized_margin", + "low_self_confidence", + ] ): - # TODO - add support for these two filters + # TODO - add support for these filters raise ValueError( - "filter_by 'confident_learning' or 'predicted_neq_given' is not supported (yet) when setting 'num_to_remove_per_class'" + "filter_by 'confident_learning', 'predicted_neq_given', 'low_normalized_margin' " + "or 'low_self_confidence' is not supported (yet) when setting 'num_to_remove_per_class'" + ) + if filter_by == "confident_learning" and isinstance(confident_joint, np.ndarray): + warn_str = ( + "The supplied `confident_joint` is ignored when `filter_by = 'confident_learning'`; confident joint will be " + "re-estimated from the given labels. To use your supplied `confident_joint`, please specify a different " + "`filter_by` value." ) + warnings.warn(warn_str) + + K = get_num_classes( + labels=labels, pred_probs=pred_probs, label_matrix=confident_joint, multi_label=multi_label + ) + # Boolean set to true if dataset is large + big_dataset = K * len(labels) > 1e8 # Set-up number of multiprocessing threads + # On Windows/macOS, when multi_label is True, multiprocessing is much slower + # even for faily large input arrays, so we default to n_jobs=1 in this case + os_name = platform.system() if n_jobs is None: - n_jobs = multiprocessing.cpu_count() + if multi_label and os_name != "Linux": + n_jobs = 1 + else: + if psutil_exists: + n_jobs = psutil.cpu_count(logical=False) # physical cores + elif big_dataset: + print( + "To default `n_jobs` to the number of physical cores for multiprocessing in find_label_issues(), please: `pip install psutil`.\n" + "Note: You can safely ignore this message. `n_jobs` only affects runtimes, results will be the same no matter its value.\n" + "Since psutil is not installed, `n_jobs` was set to the number of logical cores by default.\n" + "Disable this message by either installing psutil or specifying the `n_jobs` argument." + ) # pragma: no cover + if not n_jobs: + # either psutil does not exist + # or psutil can return None when physical cores cannot be determined + # switch to logical cores + n_jobs = multiprocessing.cpu_count() else: assert n_jobs >= 1 if multi_label: if not isinstance(labels, list): raise TypeError("`labels` must be list when `multi_label=True`.") - + warnings.warn( + "The multi_label argument to filter.find_label_issues() is deprecated and will be removed in future versions. Please use `multilabel_classification.filter.find_label_issues()` instead.", + DeprecationWarning, + ) return _find_label_issues_multilabel( labels, pred_probs, @@ -272,13 +320,8 @@ class 0, 1, ..., K-1. ) # Else this is standard multi-class classification - K = get_num_classes( - labels=labels, pred_probs=pred_probs, label_matrix=confident_joint, multi_label=multi_label - ) # Number of examples in each class of labels label_counts = value_counts_fill_missing_classes(labels, K, multi_label=multi_label) - # Boolean set to true if dataset is large - big_dataset = K * len(labels) > 1e8 # Ensure labels are of type np.ndarray() labels = np.asarray(labels) if confident_joint is None or filter_by == "confident_learning": @@ -290,6 +333,25 @@ class 0, 1, ..., K-1. multi_label=multi_label, return_indices_of_off_diagonals=True, ) + + if filter_by in ["low_normalized_margin", "low_self_confidence"]: + # TODO: consider setting adjust_pred_probs to true based on benchmarks (or adding it kwargs, or ignoring and leaving as false by default) + scores = get_label_quality_scores( + labels, + pred_probs, + method=filter_by[4:], + adjust_pred_probs=False, + ) + num_errors = num_label_issues( + labels, pred_probs, multi_label=multi_label # TODO: Check usage of multilabel + ) + # Find label issues O(nlogn) solution (mapped to boolean mask later in the method) + cl_error_indices = np.argsort(scores)[:num_errors] + # The following is the O(n) fastest solution (check for one-off errors), but the problem is if lots of the scores are identical you will overcount, + # you can end up returning more or less and they aren't ranked in the boolean form so there's no way to drop the highest scores randomly + # boundary = np.partition(scores, num_errors)[num_errors] # O(n) solution + # label_issues_mask = scores <= boundary + if filter_by in ["prune_by_noise_rate", "prune_by_class", "both"]: # Create `prune_count_matrix` with the number of examples to remove in each class and # leave at least min_examples_per_class examples per class. @@ -310,88 +372,71 @@ class 0, 1, ..., K-1. prune_count_matrix = round_preserving_row_totals(tmp) # Prepare multiprocessing shared data - if n_jobs > 1: - _labels = RawArray("I", labels) # type: ignore - _label_counts = RawArray("I", label_counts) # type: ignore - _prune_count_matrix = RawArray("I", prune_count_matrix.flatten()) # type: ignore - _pred_probs = RawArray("f", pred_probs.flatten()) # type: ignore - else: # Multiprocessing is turned off. Create tuple with all parameters - args = ( - labels, - label_counts, - prune_count_matrix, - pred_probs, - multi_label, - min_examples_per_class, - ) + # On Linux, multiprocessing is started with fork, + # so data can be shared with global vairables + COW + # On Window/macOS, processes are started with spawn, + # so data will need to be pickled to the subprocesses through input args + chunksize = max(1, K // n_jobs) + if n_jobs == 1 or os_name == "Linux": + global pred_probs_by_class, prune_count_matrix_cols + pred_probs_by_class = {k: pred_probs[labels == k] for k in range(K)} + prune_count_matrix_cols = {k: prune_count_matrix[:, k] for k in range(K)} + args = [[k, min_examples_per_class, None] for k in range(K)] + else: + args = [ + [k, min_examples_per_class, [pred_probs[labels == k], prune_count_matrix[:, k]]] + for k in range(K) + ] # Perform Pruning with threshold probabilities from BFPRT algorithm in O(n) # Operations are parallelized across all CPU processes if filter_by == "prune_by_class" or filter_by == "both": - if n_jobs > 1: # parallelize - with multiprocessing.Pool( - n_jobs, - initializer=_init, - initargs=( - _labels, - _label_counts, - _prune_count_matrix, - prune_count_matrix.shape, - _pred_probs, - pred_probs.shape, - multi_label, - min_examples_per_class, - ), - ) as p: + if n_jobs > 1: + with multiprocessing.Pool(n_jobs) as p: if verbose: # pragma: no cover print("Parallel processing label issues by class.") sys.stdout.flush() if big_dataset and tqdm_exists: label_issues_masks_per_class = list( - tqdm.tqdm(p.imap(_prune_by_class, range(K)), total=K), + tqdm.tqdm(p.imap(_prune_by_class, args, chunksize=chunksize), total=K) ) else: - label_issues_masks_per_class = p.map(_prune_by_class, range(K)) - else: # n_jobs = 1, so no parallelization - label_issues_masks_per_class = [_prune_by_class(k, args) for k in range(K)] - label_issues_mask = np.stack(label_issues_masks_per_class).any(axis=0) + label_issues_masks_per_class = p.map(_prune_by_class, args, chunksize=chunksize) + else: + label_issues_masks_per_class = [_prune_by_class(arg) for arg in args] + + label_issues_mask = np.zeros(len(labels), dtype=bool) + for k, mask in enumerate(label_issues_masks_per_class): + if len(mask) > 1: + label_issues_mask[labels == k] = mask if filter_by == "both": label_issues_mask_by_class = label_issues_mask if filter_by == "prune_by_noise_rate" or filter_by == "both": - if n_jobs > 1: # parallelize - with multiprocessing.Pool( - n_jobs, - initializer=_init, - initargs=( - _labels, - _label_counts, - _prune_count_matrix, - prune_count_matrix.shape, - _pred_probs, - pred_probs.shape, - multi_label, - min_examples_per_class, - ), - ) as p: + if n_jobs > 1: + with multiprocessing.Pool(n_jobs) as p: if verbose: # pragma: no cover print("Parallel processing label issues by noise rate.") sys.stdout.flush() if big_dataset and tqdm_exists: label_issues_masks_per_class = list( - tqdm.tqdm(p.imap(_prune_by_count, range(K)), total=K) + tqdm.tqdm(p.imap(_prune_by_count, args, chunksize=chunksize), total=K) ) else: - label_issues_masks_per_class = p.map(_prune_by_count, range(K)) - else: # n_jobs = 1, so no parallelization - label_issues_masks_per_class = [_prune_by_count(k, args) for k in range(K)] - label_issues_mask = np.stack(label_issues_masks_per_class).any(axis=0) + label_issues_masks_per_class = p.map(_prune_by_count, args, chunksize=chunksize) + else: + label_issues_masks_per_class = [_prune_by_count(arg) for arg in args] + + label_issues_mask = np.zeros(len(labels), dtype=bool) + for k, mask in enumerate(label_issues_masks_per_class): + if len(mask) > 1: + label_issues_mask[labels == k] = mask if filter_by == "both": label_issues_mask = label_issues_mask & label_issues_mask_by_class - if filter_by == "confident_learning": + if filter_by in ["confident_learning", "low_normalized_margin", "low_self_confidence"]: label_issues_mask = np.zeros(len(labels), dtype=bool) for idx in cl_error_indices: label_issues_mask[idx] = True @@ -399,12 +444,12 @@ class 0, 1, ..., K-1. if filter_by == "predicted_neq_given": label_issues_mask = find_predicted_neq_given(labels, pred_probs, multi_label=multi_label) - # Remove label issues if given label == model prediction - # TODO: consider use of _multiclass_crossval_predict() here - pred = pred_probs.argmax(axis=1) - for i, pred_label in enumerate(pred): - if pred_label == labels[i]: - label_issues_mask[i] = False + if filter_by not in ["low_self_confidence", "low_normalized_margin"]: + # Remove label issues if given label == model prediction if issues haven't been removed yet + pred = pred_probs.argmax(axis=1) + for i, pred_label in enumerate(pred): + if pred_label == labels[i]: + label_issues_mask[i] = False if verbose: print("Number of label issues found: {}".format(sum(label_issues_mask))) @@ -429,7 +474,7 @@ def _find_label_issues_multilabel( rank_by_kwargs={}, filter_by: str = "prune_by_noise_rate", frac_noise: float = 1.0, - num_to_remove_per_class: Optional[int] = None, + num_to_remove_per_class: Optional[List[int]] = None, min_examples_per_class=1, confident_joint: Optional[np.ndarray] = None, n_jobs: Optional[int] = None, @@ -440,7 +485,42 @@ def _find_label_issues_multilabel( This is done via a one-vs-rest reduction for each class and the results are subsequently aggregated across all classes. Here `labels` must be formatted as an iterable of iterables, e.g. ``List[List[int]]``. """ - per_class_issues = _find_multilabel_issues_per_class( + if filter_by in ["low_normalized_margin", "low_self_confidence"]: + num_errors = sum( + find_label_issues( + labels=labels, + pred_probs=pred_probs, + confident_joint=confident_joint, + multi_label=True, + filter_by="confident_learning", + ) + ) + + y_one, num_classes = get_onehot_num_classes(labels, pred_probs) + label_quality_scores = ml_scorer.get_label_quality_scores( + labels=y_one, + pred_probs=pred_probs, + ) + + cl_error_indices = np.argsort(label_quality_scores)[:num_errors] + label_issues_mask = np.zeros(len(labels), dtype=bool) + for idx in cl_error_indices: + label_issues_mask[idx] = True + + if return_indices_ranked_by is not None: + label_quality_scores_issues = ml_scorer.get_label_quality_scores( + labels=y_one[label_issues_mask], + pred_probs=pred_probs[label_issues_mask], + method=ml_scorer.MultilabelScorer( + base_scorer=ml_scorer.ClassLabelScorer.from_str(return_indices_ranked_by), + ), + base_scorer_kwargs=rank_by_kwargs, + ) + return cl_error_indices[np.argsort(label_quality_scores_issues)] + + return label_issues_mask + + per_class_issues = find_multilabel_issues_per_class( labels, pred_probs, return_indices_ranked_by, @@ -472,131 +552,6 @@ def _find_label_issues_multilabel( return label_issues_idx[np.argsort(label_quality_scores_issues)] -def _find_multilabel_issues_per_class( - labels: list, - pred_probs: np.ndarray, - return_indices_ranked_by: Optional[str] = None, - rank_by_kwargs={}, - filter_by: str = "prune_by_noise_rate", - frac_noise: float = 1.0, - num_to_remove_per_class: Optional[int] = None, - min_examples_per_class=1, - confident_joint: Optional[np.ndarray] = None, - n_jobs: Optional[int] = None, - verbose: bool = False, -) -> Union[np.ndarray, Tuple[List[np.ndarray], List[Any], List[np.ndarray]]]: - """ - Parameters - ---------- - labels : List[List[int]] - List of noisy labels for multi-label classification where each example can belong to multiple classes (e.g. ``labels = [[1,2],[1],[0],[],...]`` indicates the first example in dataset belongs to both class 1 and class 2. - - - pred_probs : np.ndarray - An array of shape ``(N, K)`` of model-predicted probabilities, - ``P(label=k|x)``. Each row of this matrix corresponds - to an example `x` and contains the model-predicted probabilities that - `x` belongs to each possible class, for each of the K classes. The - columns must be ordered such that these probabilities correspond to - class 0, 1, ..., K-1. They need not sum to 1.0 - - - return_indices_ranked_by : {None, 'self_confidence', 'normalized_margin', 'confidence_weighted_entropy'}, default=None - Refer to documentation for this argument in filter.find_label_issues() for details. - - rank_by_kwargs : dict, optional - Refer to documentation for this argument in filter.find_label_issues() for details. - - filter_by : {'prune_by_class', 'prune_by_noise_rate', 'both', 'confident_learning', 'predicted_neq_given'}, default='prune_by_noise_rate' - Refer to documentation for this argument in filter.find_label_issues() for details. - - frac_noise : float, default=1.0 - Refer to documentation for this argument in filter.find_label_issues() for details. - - num_to_remove_per_class : array_like - Refer to documentation for this argument in filter.find_label_issues() for details. - - min_examples_per_class : int, default=1 - Refer to documentation for this argument in filter.find_label_issues() for details. - - confident_joint : np.ndarray, optional - An array of shape ``(K, 2, 2)`` representing a one-vs-rest formatted confident joint. - Entry ``(c, i, j)`` in this array is the number of examples confidently counted into a ``(class c, noisy label=i, true label=j)`` bin, - where `i, j` are either 0 or 1 to denote whether this example belongs to class `c` or not - (recall examples can belong to multiple classes in multi-label classification). - The `confident_joint` can be computed using :py:func:`count.compute_confident_joint `. - If not provided, it is computed from the given (noisy) `labels` and `pred_probs`. - - n_jobs : optional - Refer to documentation for this argument in filter.find_label_issues() for details. - - verbose : optional - If ``True``, prints when multiprocessing happens. - - Returns - ------- - per_class_label_issues : list(np.ndarray) - If `return_indices_ranked_by` left unspecified, returns a list of boolean **masks** for the entire dataset - where ``True`` represents a label issue and ``False`` represents an example that is - accurately labeled with high confidence. - If `return_indices_ranked_by` is specified, returns a list of shorter arrays of **indices** of examples identified to have - label issues (i.e. those indices where the mask would be ``True``), sorting by likelihood that the corresponding label is correct is not supported yet. - - Note - ---- - Obtain the *indices* of label issues in your dataset by setting - `return_indices_ranked_by`. - - """ - y_one, num_classes = get_onehot_num_classes(labels, pred_probs) - if return_indices_ranked_by is None: - bissues = np.zeros(y_one.shape).astype(bool) - else: - label_issues_list = [] - labels_list = [] - pred_probs_list = [] - if confident_joint is not None: - confident_joint_shape = confident_joint.shape - if confident_joint_shape == (num_classes, num_classes): - warnings.warn( - f"The new recommended format for `confident_joint` in multi_label settings is (num_classes,2,2) as output by compute_confident_joint(...,multi_label=True). Your K x K confident_joint in the old format is being ignored." - ) - confident_joint = None - elif confident_joint_shape != (num_classes, 2, 2): - raise ValueError("confident_joint should be of shape (num_classes, 2, 2)") - for class_num, (label, pred_prob_for_class) in enumerate(zip(y_one.T, pred_probs.T)): - pred_probs_binary = stack_complement(pred_prob_for_class) - if confident_joint is None: - conf = None - else: - conf = confident_joint[class_num] - binary_label_issues = find_label_issues( - labels=label, - pred_probs=pred_probs_binary, - return_indices_ranked_by=return_indices_ranked_by, - frac_noise=frac_noise, - rank_by_kwargs=rank_by_kwargs, - filter_by=filter_by, - multi_label=False, - num_to_remove_per_class=num_to_remove_per_class, - min_examples_per_class=min_examples_per_class, - confident_joint=conf, - n_jobs=n_jobs, - verbose=verbose, - ) - - if return_indices_ranked_by is None: - bissues[:, class_num] = binary_label_issues - else: - label_issues_list.append(binary_label_issues) - labels_list.append(label) - pred_probs_list.append(pred_probs_binary) - if return_indices_ranked_by is None: - return bissues - else: - return label_issues_list, labels_list, pred_probs_list - - def _keep_at_least_n_per_class( prune_count_matrix: np.ndarray, n: int, *, frac_noise: float = 1.0 ) -> np.ndarray: @@ -807,9 +762,6 @@ class 0, 1, ..., K-1. `pred_probs` should have been computed using 3 (or label issue and ``False`` represents an example that is accurately labeled with high confidence. - Note - ---- - Multi-label classification is not supported in this method. """ assert_valid_inputs(X=None, y=labels, pred_probs=pred_probs, multi_label=False) @@ -891,7 +843,7 @@ def _get_shared_data() -> Any: # pragma: no cover # TODO figure out what the types inside args are. -def _prune_by_class(k: int, args=None) -> np.ndarray: +def _prune_by_class(args: list) -> np.ndarray: """multiprocessing Helper function for find_label_issues() that assumes globals and produces a mask for class k for each example by removing the examples with *smallest probability* of @@ -902,41 +854,34 @@ def _prune_by_class(k: int, args=None) -> np.ndarray: k : int (between 0 and num classes - 1) The class of interest.""" - if args: # Single processing - params are passed in - ( - labels, - label_counts, - prune_count_matrix, - pred_probs, - multi_label, - min_examples_per_class, - ) = args - else: # Multiprocessing - data is shared across sub-processes - ( - labels, - label_counts, - prune_count_matrix, - pred_probs, - multi_label, - min_examples_per_class, - ) = _get_shared_data() + k, min_examples_per_class, arrays = args + if arrays is None: + pred_probs = pred_probs_by_class[k] + prune_count_matrix = prune_count_matrix_cols[k] + else: + pred_probs = arrays[0] + prune_count_matrix = arrays[1] - if label_counts[k] > min_examples_per_class: # No prune if not at least min_examples_per_class - num_issues = label_counts[k] - prune_count_matrix[k][k] + label_counts = pred_probs.shape[0] + label_issues = np.zeros(label_counts, dtype=bool) + if label_counts > min_examples_per_class: # No prune if not at least min_examples_per_class + num_issues = label_counts - prune_count_matrix[k] # Get return_indices_ranked_by of the smallest prob of class k for examples with noisy label k - label_filter = np.array([k in lst for lst in labels]) if multi_label else labels == k - class_probs = pred_probs[:, k] - rank = np.partition(class_probs[label_filter], num_issues)[num_issues] - return label_filter & (class_probs < rank) - else: - warnings.warn( - f"May not flag all label issues in class: {k}, it has too few examples (see argument: `min_examples_per_class`)" - ) - return np.zeros(len(labels), dtype=bool) + # rank = np.partition(class_probs, num_issues)[num_issues] + if num_issues >= 1: + class_probs = pred_probs[:, k] + order = np.argsort(class_probs) + label_issues[order[:num_issues]] = True + return label_issues + + warnings.warn( + f"May not flag all label issues in class: {k}, it has too few examples (see argument: `min_examples_per_class`)" + ) + return label_issues # TODO figure out what the types inside args are. -def _prune_by_count(k: int, args=None) -> np.ndarray: +def _prune_by_count(args: list) -> np.ndarray: """multiprocessing Helper function for find_label_issues() that assumes globals and produces a mask for class k for each example by removing the example with noisy label k having *largest margin*, @@ -948,43 +893,34 @@ def _prune_by_count(k: int, args=None) -> np.ndarray: k : int (between 0 and num classes - 1) The true_label class of interest.""" - if args: # Single processing - params are passed in - ( - labels, - label_counts, - prune_count_matrix, - pred_probs, - multi_label, - min_examples_per_class, - ) = args - else: # Multiprocessing - data is shared across sub-processes - ( - labels, - label_counts, - prune_count_matrix, - pred_probs, - multi_label, - min_examples_per_class, - ) = _get_shared_data() + k, min_examples_per_class, arrays = args + if arrays is None: + pred_probs = pred_probs_by_class[k] + prune_count_matrix = prune_count_matrix_cols[k] + else: + pred_probs = arrays[0] + prune_count_matrix = arrays[1] - label_issues_mask = np.zeros(len(pred_probs), dtype=bool) - pred_probs_k = pred_probs[:, k] - K = get_num_classes(labels, pred_probs, multi_label=multi_label) - if label_counts[k] <= min_examples_per_class: # No prune if not at least min_examples_per_class + label_counts = pred_probs.shape[0] + label_issues_mask = np.zeros(label_counts, dtype=bool) + if label_counts <= min_examples_per_class: warnings.warn( f"May not flag all label issues in class: {k}, it has too few examples (see `min_examples_per_class` argument)" ) - return np.zeros(len(labels), dtype=bool) - for j in range(K): # j is true label index (k is noisy label index) - num2prune = prune_count_matrix[j][k] + return label_issues_mask + + K = pred_probs.shape[1] + if K < 1: + raise ValueError("Must have at least 1 class.") + for j in range(K): + num2prune = prune_count_matrix[j] # Only prune for noise rates, not diagonal entries if k != j and num2prune > 0: # num2prune's largest p(true class k) - p(noisy class k) # for x with true label j - margin = pred_probs[:, j] - pred_probs_k - label_filter = np.array([k in lst for lst in labels]) if multi_label else labels == k - cut = -np.partition(-margin[label_filter], num2prune - 1)[num2prune - 1] - label_issues_mask = label_issues_mask | (label_filter & (margin >= cut)) + margin = pred_probs[:, j] - pred_probs[:, k] + order = np.argsort(-margin) + label_issues_mask[order[:num2prune]] = True return label_issues_mask diff --git a/cleanlab/internal/constants.py b/cleanlab/internal/constants.py new file mode 100644 index 0000000000..a848d48508 --- /dev/null +++ b/cleanlab/internal/constants.py @@ -0,0 +1,49 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . + + +FLOATING_POINT_COMPARISON = 1e-6 # floating point comparison for fuzzy equals +CLIPPING_LOWER_BOUND = 1e-6 # lower-bound clipping threshold for expected behavior +CONFIDENT_THRESHOLDS_LOWER_BOUND = ( + 2 * FLOATING_POINT_COMPARISON +) # lower bound imposed to clip confident thresholds from below, has to be larger than floating point comparison +TINY_VALUE = 1e-100 # very tiny value for clipping + + +# Object Detection Constants +EUC_FACTOR = 0.1 # Factor to control magnitude of euclidian distance. Increasing the factor makes the distances between two objects go to zero more rapidly. +MAX_ALLOWED_BOX_PRUNE = 0.97 # This is max allowed percent of boxes that are pruned before a warning is thrown given a specific threshold. Pruning too many boxes negatively affects performance. + +ALPHA = 0.9 # Param for objectlab, weight between IoU and distance when considering similarity matrix. High alpha means considering IoU more strongly over distance +LOW_PROBABILITY_THRESHOLD = 0.5 # Param for get_label_quality_score, lowest predicted class probability threshold allowed when considering predicted boxes to identify badly located label boxes. +HIGH_PROBABILITY_THRESHOLD = 0.95 # Param for objectlab, high probability threshold for considering predicted boxes to identify overlooked and swapped label boxes +TEMPERATURE = 0.1 # Param for objectlab, temperature of the softmin function used to pool the per-box quality scores for an error subtype across all boxes into a single subtype score for the image. With a lower temperature, softmin pooling acts more like minimum pooling, alternatively it acts more like mean pooling with high temperature. + +OVERLOOKED_THRESHOLD = 0.3 # Param for find_label_issues. Per-box label quality score threshold to determine max score for a box to be considered an overlooked issue +BADLOC_THRESHOLD = 0.3 # Param for find_label_issues. Per-box label quality score threshold to determine max score for a box to be considered a bad location issue +SWAP_THRESHOLD = 0.3 # Param for find_label_issues. Per-box label quality score threshold to determine max score for a box to be considered a swap issue + +CUSTOM_SCORE_WEIGHT_OVERLOOKED = ( + 1 / 3 +) # Param for get_label_quality_score, weight to determine how much to value overlooked scores over other subtypes when deciding the overall label quality score for an image. +CUSTOM_SCORE_WEIGHT_BADLOC = ( + 1 / 3 +) # Param for get_label_quality_score, weight to determine how much to value badloc scores over other subtypes when deciding issues +CUSTOM_SCORE_WEIGHT_SWAP = ( + 1 / 3 +) # Param for get_label_quality_score, weight to determine how much to value swap scores over other subtypes when deciding issues + +MAX_CLASS_TO_SHOW = 10 # Nmber of classes to show in legend during the visualize method. Classes over max_class_to_show are cut off. diff --git a/cleanlab/internal/label_quality_utils.py b/cleanlab/internal/label_quality_utils.py index 2fbead808d..1215ee665a 100644 --- a/cleanlab/internal/label_quality_utils.py +++ b/cleanlab/internal/label_quality_utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -14,12 +14,11 @@ # You should have received a copy of the GNU Affero General Public License # along with cleanlab. If not, see . -""" -Helper methods used internally for computing label quality scores -""" - +"""Helper methods used internally for computing label quality scores.""" +import warnings import numpy as np from typing import Optional +from scipy.special import xlogy from cleanlab.count import get_confident_thresholds @@ -30,9 +29,12 @@ def _subtract_confident_thresholds( multi_label: bool = False, confident_thresholds: Optional[np.ndarray] = None, ) -> np.ndarray: - """Returns adjusted predicted probabilities by subtracting the class confident thresholds and renormalizing. + """ + Return adjusted predicted probabilities by subtracting the class confident thresholds and renormalizing. + The confident class threshold for a class j is the expected (average) "self-confidence" for class j. The purpose of this adjustment is to handle class imbalance. + Parameters ---------- labels : np.ndarray @@ -52,39 +54,36 @@ def _subtract_confident_thresholds( the total number of errors considered is based on the number of labels, not the number of examples. So, the calibrated `confident_joint` will sum to the number of total labels. + Returns ------- pred_probs_adj : np.ndarray (float) Adjusted pred_probs. """ - # Get expected (average) self-confidence for each class # TODO: Test this for multi-label if confident_thresholds is None: if labels is None: raise ValueError( - f"Cannot calculate confident_thresholds without labels. Pass in either labels or already calculated " - f"confident_thresholds parameter. " - ) - else: - confident_thresholds = get_confident_thresholds( - labels, pred_probs, multi_label=multi_label + "Cannot calculate confident_thresholds without labels. Pass in either labels or already calculated " + "confident_thresholds parameter. " ) + confident_thresholds = get_confident_thresholds(labels, pred_probs, multi_label=multi_label) # Subtract the class confident thresholds pred_probs_adj = pred_probs - confident_thresholds # Re-normalize by shifting data to take care of negative values from the subtraction pred_probs_adj += confident_thresholds.max() - pred_probs_adj /= pred_probs_adj.sum(axis=1)[ - :, None - ] # The [:, None] adds a dimension to make the /= operator work for broadcasting. + pred_probs_adj /= pred_probs_adj.sum(axis=1, keepdims=True) return pred_probs_adj -def get_normalized_entropy(pred_probs: np.ndarray, min_allowed_prob: float = 1e-6) -> np.ndarray: - """Returns the normalized entropy of pred_probs. +def get_normalized_entropy( + pred_probs: np.ndarray, min_allowed_prob: Optional[float] = None +) -> np.ndarray: + """Return the normalized entropy of pred_probs. Normalized entropy is between 0 and 1. Higher values of entropy indicate higher uncertainty in the model's prediction of the correct label. @@ -96,22 +95,39 @@ def get_normalized_entropy(pred_probs: np.ndarray, min_allowed_prob: float = 1e- Parameters ---------- - pred_probs: + pred_probs : np.ndarray (shape (N, K)) Each row of this matrix corresponds to an example x and contains the model-predicted probabilities that x belongs to each possible class: P(label=k|x) - min_allowed_prob: - Minimum allowed probability value. Entries of `pred_probs` below this value will be clipped to this value. - Ensures entropy remains well-behaved even when `pred_probs` contains zeros. + min_allowed_prob : float, default: None, deprecated + Minimum allowed probability value. If not `None` (default), + entries of `pred_probs` below this value will be clipped to this value. + + .. deprecated:: 2.5.0 + This keyword is deprecated and should be left to the default. + The entropy is well-behaved even if `pred_probs` contains zeros, + clipping is unnecessary and (slightly) changes the results. Returns ------- - entropy: + entropy : np.ndarray (shape (N, )) Each element is the normalized entropy of the corresponding row of ``pred_probs``. - """ + Raises + ------ + ValueError + An error is raised if any of the probabilities is not in the interval [0, 1]. + """ + if np.any(pred_probs < 0) or np.any(pred_probs > 1): + raise ValueError("All probabilities are required to be in the interval [0, 1].") num_classes = pred_probs.shape[1] + if min_allowed_prob is not None: + warnings.warn( + "Using `min_allowed_prob` is not necessary anymore and will be removed.", + DeprecationWarning, + ) + pred_probs = np.clip(pred_probs, a_min=min_allowed_prob, a_max=None) + # Note that dividing by log(num_classes) changes the base of the log which rescales entropy to 0-1 range - clipped_pred_probs = np.clip(pred_probs, a_min=min_allowed_prob, a_max=None) - return -np.sum(pred_probs * np.log(clipped_pred_probs), axis=1) / np.log(num_classes) + return -np.sum(xlogy(pred_probs, pred_probs), axis=1) / np.log(num_classes) diff --git a/cleanlab/internal/latent_algebra.py b/cleanlab/internal/latent_algebra.py index 20e1c62de2..d42c16c436 100644 --- a/cleanlab/internal/latent_algebra.py +++ b/cleanlab/internal/latent_algebra.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -18,8 +18,8 @@ """ Contains mathematical functions relating the latent terms, ``P(given_label)``, ``P(given_label | true_label)``, ``P(true_label | given_label)``, ``P(true_label)``, etc. together. -For every function here, if the inputs are exact, the output is guaranteed to be exact. -Every function herein is the computational equivalent of a mathematical equation having a closed, exact form. +For every function here, if the inputs are exact, the output is guaranteed to be exact. +Every function herein is the computational equivalent of a mathematical equation having a closed, exact form. If the inputs are inexact, the error will of course propagate. Throughout `K` denotes the number of classes in the classification task. """ @@ -28,7 +28,8 @@ import numpy as np from typing import Tuple -from cleanlab.internal.util import value_counts, clip_values, clip_noise_rates, TINY_VALUE +from cleanlab.internal.util import value_counts, clip_values, clip_noise_rates +from cleanlab.internal.constants import TINY_VALUE, CLIPPING_LOWER_BOUND def compute_ps_py_inv_noise_matrix( @@ -73,7 +74,7 @@ def compute_py_inv_noise_matrix(ps, noise_matrix) -> Tuple[np.ndarray, np.ndarra # No class should have probability 0, so we use .000001 # Make sure valid probabilities that sum to 1.0 - py = clip_values(py, low=1e-6, high=1.0, new_sum=1.0) + py = clip_values(py, low=CLIPPING_LOWER_BOUND, high=1.0, new_sum=1.0) # All the work is done in this function (below) return py, compute_inv_noise_matrix(py=py, noise_matrix=noise_matrix, ps=ps) @@ -150,7 +151,7 @@ def compute_noise_matrix_from_inverse(ps, inverse_noise_matrix, *, py=None) -> n Returns ------- - noise_matrix : np.ndarray + noise_matrix : np.ndarray Array of shape ``(K, K)``, where `K` = number of classes, whose columns sum to 1. A conditional probability matrix of the form ``P(label=k_s|true_label=k_y)`` containing the fraction of examples in every class, labeled as every other class. @@ -267,8 +268,8 @@ def compute_py( err += " should be in [cnt, eqn, marginal, marginal_ps]" raise ValueError(err) - # Clip py (0,1), s.t. no class should have prob 0, hence 1e-5 - py = clip_values(py, low=1e-5, high=1.0, new_sum=1.0) + # Clip py (0,1), s.t. no class should have prob 0, hence 1e-6 + py = clip_values(py, low=CLIPPING_LOWER_BOUND, high=1.0, new_sum=1.0) return py diff --git a/cleanlab/internal/multiannotator_utils.py b/cleanlab/internal/multiannotator_utils.py index 42e34d7903..429321b13e 100644 --- a/cleanlab/internal/multiannotator_utils.py +++ b/cleanlab/internal/multiannotator_utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -129,17 +129,23 @@ def assert_valid_inputs_multiannotator( def assert_valid_pred_probs( - pred_probs: np.ndarray, + pred_probs: Optional[np.ndarray] = None, pred_probs_unlabeled: Optional[np.ndarray] = None, ensemble: bool = False, ): - """Validate format of pred_probs for multiannotator functions""" + """Validate format of pred_probs for multiannotator active learning functions""" + if pred_probs is None and pred_probs_unlabeled is None: + raise ValueError( + "pred_probs and pred_probs_unlabeled cannot both be None, specify at least one of the two." + ) + if ensemble: - if pred_probs.ndim != 3: - error_message = "pred_probs must be a 3d array." - if pred_probs.ndim == 2: # pragma: no cover - error_message += " If you have a 2d pred_probs array, use the non-ensemble version of this function." - raise ValueError(error_message) + if pred_probs is not None: + if pred_probs.ndim != 3: + error_message = "pred_probs must be a 3d array." + if pred_probs.ndim == 2: # pragma: no cover + error_message += " If you have a 2d pred_probs array (ie. only one predictor), use the non-ensemble version of this function." + raise ValueError(error_message) if pred_probs_unlabeled is not None: if pred_probs_unlabeled.ndim != 3: @@ -148,19 +154,19 @@ def assert_valid_pred_probs( error_message += " If you have a 2d pred_probs_unlabeled array, use the non-ensemble version of this function." raise ValueError(error_message) + if pred_probs is not None and pred_probs_unlabeled is not None: if pred_probs.shape[2] != pred_probs_unlabeled.shape[2]: raise ValueError( "pred_probs and pred_probs_unlabeled must have the same number of classes" ) else: - if pred_probs.ndim != 2: - error_message = "pred_probs must be a 2d array." - if pred_probs.ndim == 3: # pragma: no cover - error_message += ( - " If you have a 3d pred_probs array, use the ensemble version of this function." - ) - raise ValueError(error_message) + if pred_probs is not None: + if pred_probs.ndim != 2: + error_message = "pred_probs must be a 2d array." + if pred_probs.ndim == 3: # pragma: no cover + error_message += " If you have a 3d pred_probs array, use the ensemble version of this function." + raise ValueError(error_message) if pred_probs_unlabeled is not None: if pred_probs_unlabeled.ndim != 2: @@ -169,6 +175,7 @@ def assert_valid_pred_probs( error_message += " If you have a 3d pred_probs_unlabeled array, use the non-ensemble version of this function." raise ValueError(error_message) + if pred_probs is not None and pred_probs_unlabeled is not None: if pred_probs.shape[1] != pred_probs_unlabeled.shape[1]: raise ValueError( "pred_probs and pred_probs_unlabeled must have the same number of classes" @@ -200,7 +207,7 @@ def format_multiannotator_labels(labels: LabelLike) -> Tuple[pd.DataFrame, dict] try: unique_labels = unique_labels[~np.isnan(unique_labels)] unique_labels.sort() - except (TypeError): # np.unique / np.sort cannot handle string values or pd.NA types + except TypeError: # np.unique / np.sort cannot handle string values or pd.NA types nan_mask = np.array([(l is np.NaN) or (l is pd.NA) or (l == "nan") for l in unique_labels]) unique_labels = unique_labels[~nan_mask] unique_labels.sort() diff --git a/cleanlab/internal/multilabel_scorer.py b/cleanlab/internal/multilabel_scorer.py index 287a7a7e34..4d1cbd4d3c 100644 --- a/cleanlab/internal/multilabel_scorer.py +++ b/cleanlab/internal/multilabel_scorer.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -243,8 +243,10 @@ def softmin( def softmax(scores: np.ndarray) -> np.ndarray: """Softmax function.""" - exp_scores = np.exp(scores / temperature) - return exp_scores / np.sum(exp_scores, axis=axis, keepdims=True) + scores = scores / temperature + scores_max = np.amax(scores, axis=axis, keepdims=True) + exp_scores_shifted = np.exp(scores - scores_max) + return exp_scores_shifted / np.sum(exp_scores_shifted, axis=axis, keepdims=True) return np.einsum("ij,ij->i", s, softmax(1 - s)) @@ -488,7 +490,7 @@ def get_class_label_quality_scores( >>> labels = np.array([[0, 1, 0], [1, 0, 1]]) >>> pred_probs = np.array([[0.1, 0.9, 0.7], [0.4, 0.1, 0.6]]) >>> scorer = MultilabelScorer() # Use the default base scorer (SELF_CONFIDENCE) - >>> class_label_quality_scores = scorer.get_class_label_quality_scores(labels, pred_probs) + >>> class_label_quality_scores = scorer.get_label_quality_scores_per_class(labels, pred_probs) >>> class_label_quality_scores array([[0.9, 0.9, 0.3], [0.4, 0.9, 0.6]]) diff --git a/cleanlab/internal/multilabel_utils.py b/cleanlab/internal/multilabel_utils.py index 22df37cf8f..57fb5da17b 100644 --- a/cleanlab/internal/multilabel_utils.py +++ b/cleanlab/internal/multilabel_utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify diff --git a/cleanlab/internal/object_detection_utils.py b/cleanlab/internal/object_detection_utils.py new file mode 100644 index 0000000000..5b6168c0dc --- /dev/null +++ b/cleanlab/internal/object_detection_utils.py @@ -0,0 +1,102 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . + +""" +Helper functions used internally for object detection tasks. +""" +from typing import List, Optional, Dict, Any + +import numpy as np + + +def bbox_xyxy_to_xywh(bbox: List[float]) -> Optional[List[float]]: + """Converts bounding box coodrinate types from x1y1,x2y2 to x,y,w,h""" + if len(bbox) == 4: + x1, y1, x2, y2 = bbox + w = x2 - x1 + h = y2 - y1 + return [x1, y1, w, h] + else: + print("Wrong bbox shape", len(bbox)) + return None + + +def softmax(x: np.ndarray, temperature: float = 0.99, axis: int = 0) -> np.ndarray: + """Gets softmax of scores.""" + x = x / temperature + x_max = np.amax(x, axis=axis, keepdims=True) + exp_x_shifted = np.exp(x - x_max) + return exp_x_shifted / np.sum(exp_x_shifted, axis=axis, keepdims=True) + + +def softmin1d(scores: np.ndarray, temperature: float = 0.99, axis: int = 0) -> float: + """Returns softmin of passed in scores.""" + scores = np.array(scores) + softmax_scores = softmax(-1 * scores, temperature, axis) + return np.dot(softmax_scores, scores) + + +def assert_valid_aggregation_weights(aggregation_weights: Dict[str, Any]) -> None: + """assert aggregation weights are in the proper format""" + weights = np.array(list(aggregation_weights.values())) + if (not np.isclose(np.sum(weights), 1.0)) or (np.min(weights) < 0.0): + raise ValueError( + f"""Aggregation weights should be non-negative and must sum to 1.0 + """ + ) + + +def assert_valid_inputs( + labels: List[Dict[str, Any]], + predictions, + method: Optional[str] = None, + threshold: Optional[float] = None, +): + """Asserts proper input format.""" + if len(labels) != len(predictions): + raise ValueError( + f"labels and predictions length needs to match. len(labels) == {len(labels)} while len(predictions) == {len(predictions)}." + ) + # Typecheck labels and predictions + if not isinstance(labels[0], dict): + raise ValueError( + f"Labels has to be a list of dicts. Instead it is list of {type(labels[0])}." + ) + # check last column of predictions is probabilities ( < 1.)? + if not isinstance(predictions[0], (list, np.ndarray)): + raise ValueError( + f"Prediction has to be a list or np.ndarray. Instead it is type {type(predictions[0])}." + ) + if not predictions[0][0].shape[1] == 5: + raise ValueError( + f"Prediction values have to be of format [x1,y1,x2,y2,pred_prob]. Please refer to the documentation for predicted probabilities under object_detection.rank.get_label_quality_scores for details" + ) + + valid_methods = ["objectlab"] + if method is not None and method not in valid_methods: + raise ValueError( + f""" + {method} is not a valid object detection scoring method! + Please choose a valid scoring_method: {valid_methods} + """ + ) + + if threshold is not None and threshold > 1.0: + raise ValueError( + f""" + Threshold is a cutoff of predicted probabilities and therefore should be <= 1. + """ + ) diff --git a/cleanlab/internal/outlier.py b/cleanlab/internal/outlier.py new file mode 100644 index 0000000000..57004a0a21 --- /dev/null +++ b/cleanlab/internal/outlier.py @@ -0,0 +1,68 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . + +""" +Helper functions used internally for outlier detection tasks. +""" + +import numpy as np + + +def transform_distances_to_scores(distances: np.ndarray, k: int, t: int) -> np.ndarray: + """Returns an outlier score for each example based on its average distance to its k nearest neighbors. + + The transformation of a distance, :math:`d` , to a score, :math:`o` , is based on the following formula: + + .. math:: + o = \\exp\\left(-dt\\right) + + where :math:`t` scales the distance to a score in the range [0,1]. + + Parameters + ---------- + distances : np.ndarray + An array of distances of shape ``(N, num_neighbors)``, where N is the number of examples. + Each row contains the distances to each example's `num_neighbors` nearest neighbors. + It is assumed that each row is sorted in ascending order. + + k : int + Number of neighbors used to compute the average distance to each example. + This assumes that the second dimension of distances is k or greater, but it + uses slicing to avoid indexing errors. + + t : int + Controls transformation of distances between examples into similarity scores that lie in [0,1]. + + Returns + ------- + ood_features_scores : np.ndarray + An array of outlier scores of shape ``(N,)`` for N examples. + + Examples + -------- + >>> import numpy as np + >>> from cleanlab.outlier import transform_distances_to_scores + >>> distances = np.array([[0.0, 0.1, 0.25], + ... [0.15, 0.2, 0.3]]) + >>> transform_distances_to_scores(distances, k=2, t=1) + array([0.95122942, 0.83945702]) + """ + # Calculate average distance to k-nearest neighbors + avg_knn_distances = distances[:, :k].mean(axis=1) + + # Map ood_features_scores to range 0-1 with 0 = most concerning + ood_features_scores: np.ndarray = np.exp(-1 * avg_knn_distances * t) + return ood_features_scores diff --git a/cleanlab/internal/regression_utils.py b/cleanlab/internal/regression_utils.py new file mode 100644 index 0000000000..50e096db9d --- /dev/null +++ b/cleanlab/internal/regression_utils.py @@ -0,0 +1,130 @@ +# Copyright (C) 2017-2022 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . + + +""" +Helper functions internally used in cleanlab.regression. +""" + +import numpy as np +import pandas as pd +from numpy.typing import ArrayLike +from typing import Tuple, Union + + +def assert_valid_prediction_inputs( + labels: ArrayLike, + predictions: ArrayLike, + method: str, +) -> Tuple[np.ndarray, np.ndarray]: + """Checks that ``labels``, ``predictions``, ``method`` are correctly formatted.""" + + # Load array_like input as numpy array. If not raise error. + try: + labels = np.asarray(labels) + except: + raise ValueError(f"labels must be array_like.") + + try: + predictions = np.asarray(predictions) + except: + raise ValueError(f"predictions must be array_like.") + + # Check if labels and predictions are 1-D and numeric + valid_labels = check_dimension_and_datatype(check_input=labels, text="labels") + valid_predictions = check_dimension_and_datatype(check_input=predictions, text="predictions") + + # Check if number of examples are same. + assert ( + valid_labels.shape == valid_predictions.shape + ), f"Number of examples in labels {labels.shape} and predictions {predictions.shape} are not same." + + # Check if inputs have missing values + check_missing_values(valid_labels, text="labels") + check_missing_values(valid_predictions, text="predictions") + + # Check if method is among allowed scoring method + scoring_methods = ["residual", "outre"] + if method not in scoring_methods: + raise ValueError(f"Specified method '{method}' must be one of: {scoring_methods}.") + + # return 1-D numpy array + return valid_labels, valid_predictions + + +def assert_valid_regression_inputs( + X: Union[np.ndarray, pd.DataFrame], + y: ArrayLike, +) -> Tuple[np.ndarray, np.ndarray]: + """ + Checks that regression inputs are properly formatted and returns the inputs in numpy array format. + """ + try: + X = np.asarray(X) + except: + raise ValueError(f"X must be array_like.") + + y = check_dimension_and_datatype(y, "y") + check_missing_values(y, text="y") + + if len(X) != len(y): + raise ValueError("X and y must have same length.") + + return X, y + + +def check_dimension_and_datatype(check_input: ArrayLike, text: str) -> np.ndarray: + """ + Raises errors related to: + 1. If input is empty + 2. If input is not 1-D + 3. If input is not numeric + + If all the checks are passed, it returns the squeezed 1-D array required by the main algorithm. + """ + + try: + check_input = np.asarray(check_input) + except: + raise ValueError(f"{text} could not be converted to numpy array, check input.") + + # Check if input is empty + if not check_input.size: + raise ValueError(f"{text} cannot be empty array.") + + # Remove axis with length one + check_input = np.squeeze(check_input) + + # Check if input is 1-D + if check_input.ndim != 1: + raise ValueError( + f"Expected 1-Dimensional inputs for {text}, got {check_input.ndim} dimensions." + ) + + # Check if datatype is numeric + if not np.issubdtype(check_input.dtype, np.number): + raise ValueError( + f"Expected {text} to contain numeric values, got values of type {check_input.dtype}." + ) + + return check_input + + +def check_missing_values(check_input: np.ndarray, text: str): + """Raise error if there are any missing values in Numpy array.""" + + if np.isnan(check_input).any(): + raise ValueError(f"{text} cannot contain missing values.") diff --git a/cleanlab/internal/segmentation_utils.py b/cleanlab/internal/segmentation_utils.py new file mode 100644 index 0000000000..f1354273b3 --- /dev/null +++ b/cleanlab/internal/segmentation_utils.py @@ -0,0 +1,70 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . + +""" +Helper functions used internally for segmentation tasks. +""" +from typing import Optional, List + +import numpy as np + + +def _get_valid_optional_params( + batch_size: Optional[int] = None, + n_jobs: Optional[int] = None, +): + """Takes in optional args and returns good values for them if they are None.""" + if batch_size is None: + batch_size = 10000 + return batch_size, n_jobs + + +def _get_summary_optional_params( + class_names: Optional[List[str]] = None, + exclude: Optional[List[int]] = None, + top: Optional[int] = None, +): + """Takes in optional args and returns good values for them if they are None for summary functions.""" + if exclude is None: + exclude = [] + if top is None: + top = 20 + return class_names, exclude, top + + +def _check_input(labels: np.ndarray, pred_probs: np.ndarray) -> None: + """ + Checks that the input labels and predicted probabilities are valid. + + Parameters + ---------- + labels: + Array of shape ``(N, H, W)`` of integer labels, where `N` is the number of images in the dataset and `H` and `W` are the height and width of the images. + + pred_probs: + Array of shape ``(N, K, H, W)`` of predicted probabilities, where `N` is the number of images in the dataset, `K` is the number of classes, and `H` and `W` are the height and width of the images. + """ + if len(labels.shape) != 3: + raise ValueError("labels must have a shape of (N, H, W)") + + if len(pred_probs.shape) != 4: + raise ValueError("pred_probs must have a shape of (N, K, H, W)") + + num_images, height, width = labels.shape + num_images_pred, num_classes, height_pred, width_pred = pred_probs.shape + + if num_images != num_images_pred or height != height_pred or width != width_pred: + raise ValueError("labels and pred_probs must have matching dimensions for N, H, and W") diff --git a/cleanlab/internal/token_classification_utils.py b/cleanlab/internal/token_classification_utils.py index fdc012e3a6..d6abafea75 100644 --- a/cleanlab/internal/token_classification_utils.py +++ b/cleanlab/internal/token_classification_utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -17,12 +17,18 @@ """ Helper methods used internally in cleanlab.token_classification """ +from __future__ import annotations import re import string import numpy as np from termcolor import colored -from typing import List, Optional, Callable, Tuple +from typing import List, Optional, Callable, Tuple, TypeVar, TYPE_CHECKING + +if TYPE_CHECKING: # pragma: no cover + import numpy.typing as npt + + T = TypeVar("T", bound=npt.NBitBase) def get_sentence(words: List[str]) -> str: @@ -171,14 +177,16 @@ def mapping(entities: List[int], maps: List[int]) -> List[int]: return list(map(f, entities)) -def merge_probs(probs: np.ndarray, maps: List[int]) -> np.ndarray: +def merge_probs( + probs: npt.NDArray["np.floating[T]"], maps: List[int] +) -> npt.NDArray["np.floating[T]"]: """ Merges model-predictive probabilities with desired mapping Parameters ---------- probs: - np.array of shape `(N, K)`, where N is the number of tokens, and K is the number of classes for the model + A 2D np.array of shape `(N, K)`, where N is the number of tokens, and K is the number of classes for the model maps: a list of mapped index, such that the probability of the token being in the i'th class is mapped to the @@ -188,7 +196,7 @@ def merge_probs(probs: np.ndarray, maps: List[int]) -> np.ndarray: Returns --------- probs_merged: - np.array of shape ``(N, K')``, where `K` is the number of new classes. Probabilities are merged and + A 2D np.array of shape ``(N, K')``, where `K'` is the number of new classes. Probabilities are merged and re-normalized if necessary. Examples diff --git a/cleanlab/internal/util.py b/cleanlab/internal/util.py index ea7ef5f8c3..ee2d53f2b0 100644 --- a/cleanlab/internal/util.py +++ b/cleanlab/internal/util.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -25,9 +25,7 @@ from cleanlab.typing import DatasetLike, LabelLike from cleanlab.internal.validation import labels_to_array - - -TINY_VALUE = 1e-100 +from cleanlab.internal.constants import FLOATING_POINT_COMPARISON, TINY_VALUE def remove_noise_from_class(noise_matrix, class_without_noise) -> np.ndarray: @@ -234,7 +232,7 @@ def round_preserving_sum(iterable) -> np.ndarray: orig_sum = np.sum(floats).round() int_sum = np.sum(ints).round() # Adjust the integers so that they sum to orig_sum - while abs(int_sum - orig_sum) > 1e-6: + while abs(int_sum - orig_sum) > FLOATING_POINT_COMPARISON: diff = np.round(orig_sum - int_sum) increment = -1 if int(diff < 0.0) else 1 changes = min(int(abs(diff)), len(iterable)) diff --git a/cleanlab/internal/validation.py b/cleanlab/internal/validation.py index 5ce017d1d7..f36603515f 100644 --- a/cleanlab/internal/validation.py +++ b/cleanlab/internal/validation.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -19,6 +19,7 @@ """ from cleanlab.typing import LabelLike, DatasetLike +from cleanlab.internal.constants import FLOATING_POINT_COMPARISON from typing import Any, List, Optional, Union import warnings import numpy as np @@ -95,7 +96,9 @@ def assert_valid_inputs( f"pred_probs must have at least {highest_class} columns, based on the largest class index which appears in labels." ) # Check for valid probabilities. - if (np.min(pred_probs) < 0) or (np.max(pred_probs) > 1): + if (np.min(pred_probs) < 0 - FLOATING_POINT_COMPARISON) or ( + np.max(pred_probs) > 1 + FLOATING_POINT_COMPARISON + ): raise ValueError("Values in pred_probs must be between 0 and 1.") if X is not None: warnings.warn("When X and pred_probs are both provided, the former may be ignored.") diff --git a/cleanlab/models/README.md b/cleanlab/models/README.md new file mode 100644 index 0000000000..6551aa13ea --- /dev/null +++ b/cleanlab/models/README.md @@ -0,0 +1,11 @@ +# Useful models adapted for use with cleanlab + +Methods in this ``models`` module are not guaranteed to be stable between different ``cleanlab`` versions. + +Some of these files include various models that can be used with cleanlab to find issues in specific types of data. These require dependencies on deep learning and other machine learning packages that are not official cleanlab dependencies. You must install these dependencies on your own if you wish to use them. + +The dependencies are as follows: +* keras.py - a wrapper to make any Keras model compatible with cleanlab and sklearn + - tensorflow +* fasttext.py - a cleanlab-compatible FastText classifier for text data + - fasttext diff --git a/cleanlab/models/__init__.py b/cleanlab/models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cleanlab/experimental/fasttext.py b/cleanlab/models/fasttext.py similarity index 92% rename from cleanlab/experimental/fasttext.py rename to cleanlab/models/fasttext.py index 6247e3cf97..3b2cd66259 100644 --- a/cleanlab/experimental/fasttext.py +++ b/cleanlab/models/fasttext.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -15,10 +15,16 @@ # along with cleanlab. If not, see . """ -Text classification with FastText models that are compatible with cleanlab. +Text classification with fastText models that are compatible with cleanlab. This module allows you to easily find label issues in your text datasets. -You must first ``pip install fasttext`` +You must have fastText installed: ``pip install fasttext``. + +Tips: + +* Check out our example using this class: `fasttext_amazon_reviews `_ +* Our `unit tests `_ also provide basic usage examples. + """ import time @@ -96,6 +102,17 @@ def _split_labels_and_text(batch): class FastTextClassifier(BaseEstimator): # Inherits sklearn base classifier + """Instantiate a fastText classifier that is compatible with :py:class:`CleanLearning `. + + Parameters + ---------- + train_data_fn: str + File name of the training data in the format compatible with fastText. + + test_data_fn: str, optional + File name of the test data in the format compatible with fastText. + """ + def __init__( self, train_data_fn, @@ -151,7 +168,7 @@ def _create_train_data(self, data_indices): masked_fn = "fastTextClf_" + str(int(time.time())) + ".txt" open(masked_fn, "w").close() # Read in training data one line at a time - with open(self.train_data_fn, "rU") as rf: + with open(self.train_data_fn, "r") as rf: idx = 0 data_idx = data_indices.pop() for line in rf: diff --git a/cleanlab/experimental/keras.py b/cleanlab/models/keras.py similarity index 57% rename from cleanlab/experimental/keras.py rename to cleanlab/models/keras.py index d4d3e8cf03..13e2442719 100644 --- a/cleanlab/experimental/keras.py +++ b/cleanlab/models/keras.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -16,26 +16,29 @@ """ Wrapper class you can use to make any Keras model compatible with :py:class:`CleanLearning ` and sklearn. -Use :py:class:`KerasWrapperModel` to wrap existing functional API code for ``keras.Model`` objects, +Use :py:class:`KerasWrapperModel` to wrap existing functional API code for ``keras.Model`` objects, and :py:class:`KerasWrapperSequential` to wrap existing ``tf.keras.models.Sequential`` objects. -Most of the instance methods of this class work the same as the ones for the wrapped Keras model, +Most of the instance methods of this class work the same as the ones for the wrapped Keras model, see the `Keras documentation `_ for details. This is a good example of making any bespoke neural network compatible with cleanlab. You must have `Tensorflow 2 installed `_ (only compatible with Python versions >= 3.7). +This wrapper class is only fully compatible with ``tensorflow<2.11``, if using ``tensorflow>=2.11``, +please replace your Optimizer class with the legacy Optimizer `here `_. Tips: * If this class lacks certain functionality, you can alternatively try `scikeras `_. * Unlike scikeras, our `KerasWrapper` classes can operate directly on ``tensorflow.data.Dataset`` objects (like regular Keras models). * To call ``fit()`` on a tensorflow ``Dataset`` object with a Keras model, the ``Dataset`` should already be batched. -* Check out our `example `_ using this class: `huggingface_keras_imdb `_ -* Our `unit tests `_ also provide basic usage examples. +* Check out our example using this class: `huggingface_keras_imdb `_ +* Our `unit tests `_ also provide basic usage examples. """ import tensorflow as tf +import keras # type: ignore import numpy as np from typing import Callable, Optional @@ -50,7 +53,7 @@ class KerasWrapperModel: Parameters ---------- model: Callable - A callable function to construct the Keras Model (using functional API). Pass in the function here, not the constructed model! + A callable function to construct the Keras Model (using functional API). Pass in the function here, not the constructed model! For example:: @@ -73,29 +76,63 @@ def __init__( compile_kwargs: dict = { "loss": tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) }, + params: Optional[dict] = None, ): + if params is None: + params = {} + self.model = model self.model_kwargs = model_kwargs self.compile_kwargs = compile_kwargs + self.params = params self.net = None def get_params(self, deep=True): + """Returns the parameters of the Keras model.""" return { "model": self.model, "model_kwargs": self.model_kwargs, "compile_kwargs": self.compile_kwargs, + "params": self.params, } + def set_params(self, **params): + """Set the parameters of the Keras model.""" + self.params.update(params) + return self + def fit(self, X, y=None, **kwargs): - """Note that `X` dataset object must already contain the labels as is required for standard Keras fit. - You can optionally provide the labels again here as argument `y` to be compatible with sklearn, but they are ignored. + """Trains a Keras model. + + Parameters + ---------- + X : tf.Dataset or np.array or pd.DataFrame + If ``X`` is a tensorflow dataset object, it must already contain the labels as is required for standard Keras fit. + + y : np.array or pd.DataFrame, default = None + If ``X`` is a tensorflow dataset object, you can optionally provide the labels again here as argument `y` to be compatible with sklearn, + but they are ignored. + If ``X`` is a numpy array or pandas dataframe, the labels have to be passed in using this argument. """ - self.net = self.model(**self.model_kwargs) - self.net.compile(**self.compile_kwargs) - self.net.fit(X, **kwargs) + if self.net is None: + self.net = self.model(**self.model_kwargs) + self.net.compile(**self.compile_kwargs) + + # TODO: check for generators + if y is not None and not isinstance(X, (tf.data.Dataset, keras.utils.Sequence)): + kwargs["y"] = y + + self.net.fit(X, **{**self.params, **kwargs}) def predict_proba(self, X, *, apply_softmax=True, **kwargs): - """Set extra argument `apply_softmax` to True to indicate your network only outputs logits not probabilities.""" + """Predict class probabilities for all classes using the wrapped Keras model. + Set extra argument `apply_softmax` to True to indicate your network only outputs logits not probabilities. + + Parameters + ---------- + X : tf.Dataset or np.array or pd.DataFrame + Data in the same format as the original ``X`` provided to ``fit()``. + """ if self.net is None: raise ValueError("must call fit() before predict()") pred_probs = self.net.predict(X, **kwargs) @@ -104,11 +141,24 @@ def predict_proba(self, X, *, apply_softmax=True, **kwargs): return pred_probs def predict(self, X, **kwargs): + """Predict class labels using the wrapped Keras model. + + Parameters + ---------- + X : tf.Dataset or np.array or pd.DataFrame + Data in the same format as the original ``X`` provided to ``fit()``. + + """ pred_probs = self.predict_proba(X, **kwargs) return np.argmax(pred_probs, axis=1) def summary(self, **kwargs): - self.net.summary(**kwargs) + """Returns the summary of the Keras model.""" + if self.net is None: + self.net = self.model(**self.model_kwargs) + self.net.compile(**self.compile_kwargs) + + return self.net.summary(**kwargs) class KerasWrapperSequential: @@ -137,29 +187,63 @@ def __init__( compile_kwargs: dict = { "loss": tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) }, + params: Optional[dict] = None, ): + if params is None: + params = {} + self.layers = layers self.name = name self.compile_kwargs = compile_kwargs + self.params = params self.net = None def get_params(self, deep=True): + """Returns the parameters of the Keras model.""" return { "layers": self.layers, "name": self.name, "compile_kwargs": self.compile_kwargs, + "params": self.params, } + def set_params(self, **params): + """Set the parameters of the Keras model.""" + self.params.update(params) + return self + def fit(self, X, y=None, **kwargs): - """Note that `X` dataset object must already contain the labels as is required for standard Keras fit. - You can optionally provide the labels again here as argument `y` to be compatible with sklearn, but they are ignored. + """Trains a Sequential Keras model. + + Parameters + ---------- + X : tf.Dataset or np.array or pd.DataFrame + If ``X`` is a tensorflow dataset object, it must already contain the labels as is required for standard Keras fit. + + y : np.array or pd.DataFrame, default = None + If ``X`` is a tensorflow dataset object, you can optionally provide the labels again here as argument `y` to be compatible with sklearn, + but they are ignored. + If ``X`` is a numpy array or pandas dataframe, the labels have to be passed in using this argument. """ - self.net = tf.keras.models.Sequential(self.layers, self.name) - self.net.compile(**self.compile_kwargs) - self.net.fit(X, **kwargs) + if self.net is None: + self.net = tf.keras.models.Sequential(self.layers, self.name) + self.net.compile(**self.compile_kwargs) + + # TODO: check for generators + if y is not None and not isinstance(X, (tf.data.Dataset, keras.utils.Sequence)): + kwargs["y"] = y + + self.net.fit(X, **{**self.params, **kwargs}) def predict_proba(self, X, *, apply_softmax=True, **kwargs): - """Set extra argument `apply_softmax` to True to indicate your network only outputs logits not probabilities.""" + """Predict class probabilities for all classes using the wrapped Keras model. + Set extra argument `apply_softmax` to True to indicate your network only outputs logits not probabilities. + + Parameters + ---------- + X : tf.Dataset or np.array or pd.DataFrame + Data in the same format as the original ``X`` provided to ``fit()``. + """ if self.net is None: raise ValueError("must call fit() before predict()") pred_probs = self.net.predict(X, **kwargs) @@ -168,8 +252,20 @@ def predict_proba(self, X, *, apply_softmax=True, **kwargs): return pred_probs def predict(self, X, **kwargs): + """Predict class labels using the wrapped Keras model. + + Parameters + ---------- + X : tf.Dataset or np.array or pd.DataFrame + Data in the same format as the original ``X`` provided to ``fit()``. + """ pred_probs = self.predict_proba(X, **kwargs) return np.argmax(pred_probs, axis=1) def summary(self, **kwargs): - self.net.summary(**kwargs) + """Returns the summary of the Keras model.""" + if self.net is None: + self.net = tf.keras.models.Sequential(self.layers, self.name) + self.net.compile(**self.compile_kwargs) + + return self.net.summary(**kwargs) diff --git a/cleanlab/multiannotator.py b/cleanlab/multiannotator.py index 214cd8b247..a985cd7925 100644 --- a/cleanlab/multiannotator.py +++ b/cleanlab/multiannotator.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -25,14 +25,17 @@ * An analogous label quality score for each individual label chosen by one annotator for a particular example. * An overall quality score for each annotator which measures our confidence in the overall correctness of labels obtained from this annotator. -The underlying algorithms used to compute the statistics are described in `the CROWDLAB paper `_. +The algorithms to compute these estimates are described in `the CROWDLAB paper `_. If you have some labeled and unlabeled data (with multiple annotators for some labeled examples) and want to decide what data to collect additional labels for, use the :py:func:`get_active_learning_scores ` function, which is intended for active learning. -This function estimates an active learning quality score for each example, +This function estimates an ActiveLab quality score for each example, which can be used to prioritize which examples are most informative to collect additional labels for. -This function is effective for settings where some examples have been labeled by one or more annotators and other examples can have no labels at all so far, -as well as settings where new labels are collected either in batches of examples or one at a time. +This function is effective for settings where some examples have been labeled by one or more annotators and other examples can have no labels at all so far, +as well as settings where new labels are collected either in batches of examples or one at a time. +Here is an `example notebook `_ showcasing the use of this ActiveLab method for active learning with data re-labeling. + +The algorithms to compute these active learning scores are described in `the ActiveLab paper `_. Each of the main functions in this module utilizes any trained classifier model. Variants of these functions are provided for settings where you have trained an ensemble of multiple models. @@ -46,6 +49,8 @@ from cleanlab.rank import get_label_quality_scores from cleanlab.internal.util import get_num_classes, value_counts +from cleanlab.internal.constants import CLIPPING_LOWER_BOUND + from cleanlab.internal.multiannotator_utils import ( assert_valid_inputs_multiannotator, assert_valid_pred_probs, @@ -68,7 +73,7 @@ def get_label_quality_multiannotator( verbose: bool = True, label_quality_score_kwargs: dict = {}, ) -> Dict[str, Any]: - """Returns label quality scores for each example and for each annotator. + """Returns label quality scores for each example and for each annotator in a dataset labeled by multiple annotators. This function is for multiclass classification datasets where examples have been labeled by multiple annotators (not necessarily the same number of annotators per example). @@ -76,14 +81,14 @@ def get_label_quality_multiannotator( It computes one consensus label for each example that best accounts for the labels chosen by each annotator (and their quality), as well as a consensus quality score for how confident we are that this consensus label is actually correct. It also computes similar quality scores for each annotator's individual labels, and the quality of each annotator. - Scores are between 0 and 1; lower scores indicate labels/annotators less likely to be correct. + Scores are between 0 and 1 (estimated via methods like CROWDLAB); lower scores indicate labels/annotators less likely to be correct. To decide what data to collect additional labels for, try the :py:func:`get_active_learning_scores ` - function, which is intended for active learning with multiple annotators. + (ActiveLab) function, which is intended for active learning with multiple annotators. Parameters ---------- - labels_multiannotator : pd.DataFrame of np.ndarray + labels_multiannotator : pd.DataFrame or np.ndarray 2D pandas DataFrame or array of multiple given labels for each example with shape ``(N, M)``, where N is the number of examples and M is the number of annotators. ``labels_multiannotator[n][m]`` = label for n-th example given by m-th annotator. @@ -358,7 +363,7 @@ def get_label_quality_multiannotator_ensemble( Parameters ---------- - labels_multiannotator : pd.DataFrame of np.ndarray + labels_multiannotator : pd.DataFrame or np.ndarray Multiannotator labels in the same format expected by :py:func:`get_label_quality_multiannotator `. pred_probs : np.ndarray An array of shape ``(P, N, K)`` where P is the number of models, consisting of predicted class probabilities from the ensemble models. @@ -534,13 +539,13 @@ def get_label_quality_multiannotator_ensemble( def get_active_learning_scores( - labels_multiannotator: Union[pd.DataFrame, np.ndarray], - pred_probs: np.ndarray, + labels_multiannotator: Optional[Union[pd.DataFrame, np.ndarray]] = None, + pred_probs: Optional[np.ndarray] = None, pred_probs_unlabeled: Optional[np.ndarray] = None, ) -> Tuple[np.ndarray, np.ndarray]: - """Returns an active learning quality score for each example in the dataset. + """Returns an ActiveLab quality score for each example in the dataset, to estimate which examples are most informative to (re)label next in active learning. - We consider settings where one example can be labeled by multiple annotators and some examples have no labels at all so far. + We consider settings where one example can be labeled by one or more annotators and some examples have no labels at all so far. The score is in between 0 and 1, and can be used to prioritize what data to collect additional labels for. Lower scores indicate examples whose true label we are least confident about based on the current data; @@ -548,27 +553,35 @@ def get_active_learning_scores( To use an annotation budget most efficiently, select a batch of examples with the lowest scores and collect one additional label for each example, and repeat this process after retraining your classifier. + You can use this function to get active learning scores for: examples that already have one or more labels (specify ``labels_multiannotator`` and ``pred_probs`` + as arguments), or for unlabeled examples (specify ``pred_probs_unlabeled``), or for both types of examples (specify all of the above arguments). + To analyze a fixed dataset labeled by multiple annotators rather than collecting additional labels, try the - :py:func:`get_label_quality_multiannotator ` function instead. + :py:func:`get_label_quality_multiannotator ` (CROWDLAB) function instead. Parameters ---------- - labels_multiannotator : pd.DataFrame of np.ndarray + labels_multiannotator : pd.DataFrame or np.ndarray, optional 2D pandas DataFrame or array of multiple given labels for each example with shape ``(N, M)``, - where N is the number of examples and M is the number of annotators. + where N is the number of examples and M is the number of annotators. Note that this function also works with + datasets where there is only one annotator (M=1). For more details, labels in the same format expected by the :py:func:`get_label_quality_multiannotator `. Note that examples that have no annotator labels should not be included in this DataFrame/array. - pred_probs : np.ndarray + This argument is optional if ``pred_probs`` is not provided (you might only provide ``pred_probs_unlabeled`` to only get active learning scores for the unlabeled examples). + pred_probs : np.ndarray, optional An array of shape ``(N, K)`` of predicted class probabilities from a trained classifier model. Predicted probabilities in the same format expected by the :py:func:`get_label_quality_scores `. + This argument is optional if you only want to get active learning scores for unlabeled examples (specify only ``pred_probs_unlabeled`` instead). pred_probs_unlabeled : np.ndarray, optional An array of shape ``(N, K)`` of predicted class probabilities from a trained classifier model for examples that have no annotator labels. Predicted probabilities in the same format expected by the :py:func:`get_label_quality_scores `. + This argument is optional if you only want to get active learning scores for already-labeled examples (specify only ``pred_probs`` instead). Returns ------- active_learning_scores : np.ndarray - Array of shape ``(N,)`` indicating the active learning quality scores for each example. + Array of shape ``(N,)`` indicating the ActiveLab quality scores for each example. + This array is empty if no already-labeled data was provided via ``labels_multiannotator``. Examples with the lowest scores are those we should label next in order to maximally improve our classifier model. active_learning_scores_unlabeled : np.ndarray @@ -578,58 +591,83 @@ def get_active_learning_scores( (scores for unlabeled data are directly comparable with the `active_learning_scores` for labeled data). """ - if isinstance(labels_multiannotator, np.ndarray): - labels_multiannotator = pd.DataFrame(labels_multiannotator) - assert_valid_pred_probs(pred_probs=pred_probs, pred_probs_unlabeled=pred_probs_unlabeled) - num_classes = get_num_classes(pred_probs=pred_probs) + # compute multiannotator stats if labeled data is provided + if pred_probs is not None: + if labels_multiannotator is None: + raise ValueError( + "labels_multiannotator cannot be None when passing in pred_probs. ", + "Either provide labels_multiannotator to obtain active learning scores for the labeled examples, " + "or just pass in pred_probs_unlabeled to get active learning scores for unlabeled examples.", + ) - optimal_temp = find_best_temp_scaler(labels_multiannotator, pred_probs) - pred_probs = temp_scale_pred_probs(pred_probs, optimal_temp) + if isinstance(labels_multiannotator, np.ndarray): + labels_multiannotator = pd.DataFrame(labels_multiannotator) - # if all examples are only labeled by a single annotator - if labels_multiannotator.apply(lambda s: len(s.dropna()) == 1, axis=1).all(): - assert_valid_inputs_multiannotator( - labels_multiannotator, pred_probs, allow_single_label=True - ) + num_classes = get_num_classes(pred_probs=pred_probs) - consensus_label = get_majority_vote_label( - labels_multiannotator=labels_multiannotator, - pred_probs=pred_probs, - verbose=False, - ) - quality_of_consensus_labeled = get_label_quality_scores(consensus_label, pred_probs) + # if all examples are only labeled by a single annotator + if labels_multiannotator.apply(lambda s: len(s.dropna()) == 1, axis=1).all(): + optimal_temp = 1.0 # do not temp scale for single annotator case, temperature is defined here for later use + + assert_valid_inputs_multiannotator( + labels_multiannotator, pred_probs, allow_single_label=True + ) + + consensus_label = get_majority_vote_label( + labels_multiannotator=labels_multiannotator, + pred_probs=pred_probs, + verbose=False, + ) + quality_of_consensus_labeled = get_label_quality_scores(consensus_label, pred_probs) + model_weight = 1 + annotator_weight = np.full(labels_multiannotator.shape[1], 1) + avg_annotator_weight = np.mean(annotator_weight) + + # examples are annotated by multiple annotators + else: + optimal_temp = find_best_temp_scaler(labels_multiannotator, pred_probs) + pred_probs = temp_scale_pred_probs(pred_probs, optimal_temp) + + multiannotator_info = get_label_quality_multiannotator( + labels_multiannotator, + pred_probs, + return_annotator_stats=False, + return_detailed_quality=False, + return_weights=True, + ) + + quality_of_consensus_labeled = multiannotator_info["label_quality"][ + "consensus_quality_score" + ] + model_weight = multiannotator_info["model_weight"] + annotator_weight = multiannotator_info["annotator_weight"] + avg_annotator_weight = np.mean(annotator_weight) + + # compute scores for labeled data + active_learning_scores = np.full(len(labels_multiannotator), np.nan) + for i in range(len(active_learning_scores)): + annotator_labels = labels_multiannotator.iloc[i] + active_learning_scores[i] = np.average( + (quality_of_consensus_labeled[i], 1 / num_classes), + weights=( + np.sum(annotator_weight[annotator_labels.notna()]) + model_weight, + avg_annotator_weight, + ), + ) + + # no labeled data provided so do not estimate temperature and model/annotator weights + elif pred_probs_unlabeled is not None: + num_classes = get_num_classes(pred_probs=pred_probs_unlabeled) + optimal_temp = 1 model_weight = 1 - annotator_weight = np.full(labels_multiannotator.shape[1], 1) - avg_annotator_weight = np.mean(annotator_weight) + avg_annotator_weight = 1 + active_learning_scores = np.array([]) else: - multiannotator_info = get_label_quality_multiannotator( - labels_multiannotator, - pred_probs, - return_annotator_stats=False, - return_detailed_quality=False, - return_weights=True, - ) - - quality_of_consensus_labeled = multiannotator_info["label_quality"][ - "consensus_quality_score" - ] - model_weight = multiannotator_info["model_weight"] - annotator_weight = multiannotator_info["annotator_weight"] - avg_annotator_weight = np.mean(annotator_weight) - - # compute scores for labeled data - active_learning_scores = np.full(len(labels_multiannotator), np.nan) - for i in range(len(active_learning_scores)): - annotator_labels = labels_multiannotator.iloc[i] - active_learning_scores[i] = np.average( - (quality_of_consensus_labeled[i], 1 / num_classes), - weights=( - np.sum(annotator_weight[annotator_labels.notna()]) + model_weight, - avg_annotator_weight, - ), + raise ValueError( + "pred_probs and pred_probs_unlabeled cannot both be None, specify at least one of the two." ) # compute scores for unlabeled data @@ -655,26 +693,30 @@ def get_active_learning_scores( def get_active_learning_scores_ensemble( - labels_multiannotator: Union[pd.DataFrame, np.ndarray], - pred_probs: np.ndarray, + labels_multiannotator: Optional[Union[pd.DataFrame, np.ndarray]] = None, + pred_probs: Optional[np.ndarray] = None, pred_probs_unlabeled: Optional[np.ndarray] = None, ) -> Tuple[np.ndarray, np.ndarray]: - """Returns an active learning quality score for each example in the dataset, based on predictions from an ensemble of models. + """Returns an ActiveLab quality score for each example in the dataset, based on predictions from an ensemble of models. This function is similar to :py:func:`get_active_learning_scores ` but allows for an - ensemble of multiple classifier models to be trained and will aggregate predictions from the models to compute the active learning quality score. + ensemble of multiple classifier models to be trained and will aggregate predictions from the models to compute the ActiveLab quality score. Parameters ---------- labels_multiannotator : pd.DataFrame or np.ndarray Multiannotator labels in the same format expected by :py:func:`get_active_learning_scores `. + This argument is optional if ``pred_probs`` is not provided (in cases where you only provide ``pred_probs_unlabeled`` to get active learning scores for unlabeled examples). pred_probs : np.ndarray An array of shape ``(P, N, K)`` where P is the number of models, consisting of predicted class probabilities from the ensemble models. + Note that this function also works with datasets where there is only one annotator (M=1). Each set of predicted probabilities with shape ``(N, K)`` is in the same format expected by the :py:func:`get_label_quality_scores `. + This argument is optional if you only want to get active learning scores for unlabeled examples (pass in ``pred_probs_unlabeled`` instead). pred_probs_unlabeled : np.ndarray, optional An array of shape ``(P, N, K)`` where P is the number of models, consisting of predicted class probabilities from a trained classifier model for examples that have no annotated labels so far (but which we may want to label in the future, and hence compute active learning quality scores for). Each set of predicted probabilities with shape ``(N, K)`` is in the same format expected by the :py:func:`get_label_quality_scores `. + This argument is optional if you only want to get active learning scores for labeled examples (pass in ``pred_probs`` instead). Returns ------- @@ -688,66 +730,91 @@ def get_active_learning_scores_ensemble( get_active_learning_scores """ - if isinstance(labels_multiannotator, np.ndarray): - labels_multiannotator = pd.DataFrame(labels_multiannotator) - assert_valid_pred_probs( pred_probs=pred_probs, pred_probs_unlabeled=pred_probs_unlabeled, ensemble=True ) - num_classes = get_num_classes(pred_probs=pred_probs[0]) + # compute multiannotator stats if labeled data is provided + if pred_probs is not None: + if labels_multiannotator is None: + raise ValueError( + "labels_multiannotator cannot be None when passing in pred_probs. ", + "You can either provide labels_multiannotator to obtain active learning scores for the labeled examples, " + "or just pass in pred_probs_unlabeled to get active learning scores for unlabeled examples.", + ) - # temp scale pred_probs - optimal_temp = np.full(len(pred_probs), np.NaN) - for i in range(len(pred_probs)): - curr_pred_probs = pred_probs[i] - curr_optimal_temp = find_best_temp_scaler(labels_multiannotator, curr_pred_probs) - pred_probs[i] = temp_scale_pred_probs(curr_pred_probs, curr_optimal_temp) - optimal_temp[i] = curr_optimal_temp - - # if all examples are only labeled by a single annotator - if labels_multiannotator.apply(lambda s: len(s.dropna()) == 1, axis=1).all(): - assert_valid_inputs_multiannotator( - labels_multiannotator, pred_probs, ensemble=True, allow_single_label=True - ) + if isinstance(labels_multiannotator, np.ndarray): + labels_multiannotator = pd.DataFrame(labels_multiannotator) - avg_pred_probs = np.mean(pred_probs, axis=0) - consensus_label = get_majority_vote_label( - labels_multiannotator=labels_multiannotator, - pred_probs=avg_pred_probs, - verbose=False, - ) - quality_of_consensus_labeled = get_label_quality_scores(consensus_label, avg_pred_probs) - model_weight = np.full(len(pred_probs), 1) - annotator_weight = np.full(labels_multiannotator.shape[1], 1) - avg_annotator_weight = np.mean(annotator_weight) + num_classes = get_num_classes(pred_probs=pred_probs[0]) - else: - multiannotator_info = get_label_quality_multiannotator_ensemble( - labels_multiannotator, - pred_probs, - return_annotator_stats=False, - return_detailed_quality=False, - return_weights=True, - ) + # if all examples are only labeled by a single annotator + if labels_multiannotator.apply(lambda s: len(s.dropna()) == 1, axis=1).all(): + # do not temp scale for single annotator case, temperature is defined here for later use + optimal_temp = np.full(len(pred_probs), 1.0) - quality_of_consensus_labeled = multiannotator_info["label_quality"][ - "consensus_quality_score" - ] - model_weight = multiannotator_info["model_weight"] - annotator_weight = multiannotator_info["annotator_weight"] - avg_annotator_weight = np.mean(annotator_weight) - - # compute scores for labeled data - active_learning_scores = np.full(len(labels_multiannotator), np.nan) - for i in range(len(active_learning_scores)): - annotator_labels = labels_multiannotator.iloc[i] - active_learning_scores[i] = np.average( - (quality_of_consensus_labeled[i], 1 / num_classes), - weights=( - np.sum(annotator_weight[annotator_labels.notna()]) + np.sum(model_weight), - avg_annotator_weight, - ), + assert_valid_inputs_multiannotator( + labels_multiannotator, pred_probs, ensemble=True, allow_single_label=True + ) + + avg_pred_probs = np.mean(pred_probs, axis=0) + consensus_label = get_majority_vote_label( + labels_multiannotator=labels_multiannotator, + pred_probs=avg_pred_probs, + verbose=False, + ) + quality_of_consensus_labeled = get_label_quality_scores(consensus_label, avg_pred_probs) + model_weight = np.full(len(pred_probs), 1) + annotator_weight = np.full(labels_multiannotator.shape[1], 1) + avg_annotator_weight = np.mean(annotator_weight) + + # examples are annotated by multiple annotators + else: + optimal_temp = np.full(len(pred_probs), np.NaN) + for i in range(len(pred_probs)): + curr_pred_probs = pred_probs[i] + curr_optimal_temp = find_best_temp_scaler(labels_multiannotator, curr_pred_probs) + pred_probs[i] = temp_scale_pred_probs(curr_pred_probs, curr_optimal_temp) + optimal_temp[i] = curr_optimal_temp + + multiannotator_info = get_label_quality_multiannotator_ensemble( + labels_multiannotator, + pred_probs, + return_annotator_stats=False, + return_detailed_quality=False, + return_weights=True, + ) + + quality_of_consensus_labeled = multiannotator_info["label_quality"][ + "consensus_quality_score" + ] + model_weight = multiannotator_info["model_weight"] + annotator_weight = multiannotator_info["annotator_weight"] + avg_annotator_weight = np.mean(annotator_weight) + + # compute scores for labeled data + active_learning_scores = np.full(len(labels_multiannotator), np.nan) + for i in range(len(active_learning_scores)): + annotator_labels = labels_multiannotator.iloc[i] + active_learning_scores[i] = np.average( + (quality_of_consensus_labeled[i], 1 / num_classes), + weights=( + np.sum(annotator_weight[annotator_labels.notna()]) + np.sum(model_weight), + avg_annotator_weight, + ), + ) + + # no labeled data provided so do not estimate temperature and model/annotator weights + elif pred_probs_unlabeled is not None: + num_classes = get_num_classes(pred_probs=pred_probs_unlabeled[0]) + optimal_temp = np.full(len(pred_probs_unlabeled), 1.0) + model_weight = np.full(len(pred_probs_unlabeled), 1) + avg_annotator_weight = 1 + active_learning_scores = np.array([]) + + else: + raise ValueError( + "pred_probs and pred_probs_unlabeled cannot both be None, specify at least one of the two." ) # compute scores for unlabeled data @@ -848,6 +915,7 @@ def get_majority_vote_label( tied_idx[idx] = label_mode[max_pred_probs] # tiebreak 2: using empirical class frequencies + # current tiebreak will select the minority class (to prevent larger class imbalance) if len(tied_idx) > 0: if pred_probs is not None: num_classes = pred_probs.shape[1] @@ -859,14 +927,14 @@ def get_majority_vote_label( lambda s: pd.Series(np.bincount(s[s.notna()], minlength=num_classes)), axis=1 ).sum() for idx, label_mode in tied_idx.copy().items(): - max_frequency = np.where( - class_frequencies[label_mode] == np.max(class_frequencies[label_mode]) + min_frequency = np.where( + class_frequencies[label_mode] == np.min(class_frequencies[label_mode]) )[0] - if len(max_frequency) == 1: - majority_vote_label[idx] = label_mode[max_frequency[0]] + if len(min_frequency) == 1: + majority_vote_label[idx] = label_mode[min_frequency[0]] del tied_idx[idx] else: - tied_idx[idx] = label_mode[max_frequency] + tied_idx[idx] = label_mode[min_frequency] # tiebreak 3: using initial annotator quality scores if len(tied_idx) > 0: @@ -875,7 +943,13 @@ def get_majority_vote_label( annotator_agreement_with_consensus = nontied_labels_multiannotator.apply( lambda s: np.mean(s[pd.notna(s)] == nontied_majority_vote_label[pd.notna(s)]), axis=0, - ).to_numpy() + ) + + # impute average annotator accuracy for any annotator that do not overlap with consensus + mask = annotator_agreement_with_consensus.isna() + avg_annotator_agreement = np.mean(annotator_agreement_with_consensus[~mask]) + annotator_agreement_with_consensus[mask] = avg_annotator_agreement + for idx, label_mode in tied_idx.copy().items(): label_quality_score = np.array( [ @@ -1221,7 +1295,6 @@ def _get_post_pred_probs_and_weights( quality_method: str = "crowdlab", verbose: bool = True, ) -> Tuple[np.ndarray, Any, Any]: - """Return the posterior predicted probabilities of each example given a specified quality method. Parameters @@ -1286,7 +1359,7 @@ def _get_post_pred_probs_and_weights( consensus_label_subset != np.argmax(np.bincount(consensus_label_subset, minlength=num_classes)) ), - a_min=1e-6, + a_min=CLIPPING_LOWER_BOUND, a_max=None, ) @@ -1296,14 +1369,14 @@ def _get_post_pred_probs_and_weights( ) annotator_error = 1 - annotator_agreement_with_annotators adjusted_annotator_agreement = np.clip( - 1 - (annotator_error / most_likely_class_error), a_min=1e-6, a_max=None + 1 - (annotator_error / most_likely_class_error), a_min=CLIPPING_LOWER_BOUND, a_max=None ) # compute model weight model_error = np.mean(np.argmax(prior_pred_probs_subset, axis=1) != consensus_label_subset) - model_weight = np.max([(1 - (model_error / most_likely_class_error)), 1e-6]) * np.sqrt( - np.mean(num_annotations) - ) + model_weight = np.max( + [(1 - (model_error / most_likely_class_error)), CLIPPING_LOWER_BOUND] + ) * np.sqrt(np.mean(num_annotations)) # compute weighted average post_pred_probs = np.full(prior_pred_probs.shape, np.nan) @@ -1412,7 +1485,7 @@ def _get_post_pred_probs_and_weights_ensemble( consensus_label_subset != np.argmax(np.bincount(consensus_label_subset, minlength=num_classes)) ), - a_min=1e-6, + a_min=CLIPPING_LOWER_BOUND, a_max=None, ) @@ -1422,7 +1495,7 @@ def _get_post_pred_probs_and_weights_ensemble( ) annotator_error = 1 - annotator_agreement_with_annotators adjusted_annotator_agreement = np.clip( - 1 - (annotator_error / most_likely_class_error), a_min=1e-6, a_max=None + 1 - (annotator_error / most_likely_class_error), a_min=CLIPPING_LOWER_BOUND, a_max=None ) # compute model weight @@ -1431,9 +1504,9 @@ def _get_post_pred_probs_and_weights_ensemble( prior_pred_probs_subset = prior_pred_probs[idx][mask] model_error = np.mean(np.argmax(prior_pred_probs_subset, axis=1) != consensus_label_subset) - model_weight[idx] = np.max([(1 - (model_error / most_likely_class_error)), 1e-6]) * np.sqrt( - np.mean(num_annotations) - ) + model_weight[idx] = np.max( + [(1 - (model_error / most_likely_class_error)), CLIPPING_LOWER_BOUND] + ) * np.sqrt(np.mean(num_annotations)) # compute weighted average post_pred_probs = np.full(prior_pred_probs[0].shape, np.nan) diff --git a/cleanlab/multilabel_classification.py b/cleanlab/multilabel_classification.py deleted file mode 100644 index 45307adbfa..0000000000 --- a/cleanlab/multilabel_classification.py +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. -# This file is part of cleanlab. -# -# cleanlab is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# cleanlab is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with cleanlab. If not, see . - -""" -Methods to rank the severity of label issues in multi-label classification datasets. -Here each example can belong to one or more classes, or none of the classes at all. -Unlike in standard multi-class classification, predicted class probabilities from model need not sum to 1 for each row in multi-label classification. -""" - -import numpy as np -from typing import List - -from cleanlab.internal.validation import assert_valid_inputs -from cleanlab.internal.util import get_num_classes -from cleanlab.internal.multilabel_scorer import MultilabelScorer, ClassLabelScorer, Aggregator -from cleanlab.internal.multilabel_utils import int2onehot - - -def get_label_quality_scores( - labels: List, - pred_probs: np.ndarray, - *, - method: str = "self_confidence", - adjust_pred_probs: bool = False, - aggregator_kwargs: dict = {"method": "exponential_moving_average", "alpha": 0.8} -) -> np.ndarray: - """Computes a label quality score each example in a multi-label classification dataset. - - Scores are between 0 and 1 with lower scores indicating examples whose label more likely contains an error. - For each example, this method internally computes a separate score for each individual class - and then aggregates these per-class scores into an overall label quality score for the example. - - To estimate exactly which examples are mislabeled in a multi-label classification dataset, - you can also use :py:func:`filter.find_label_issues ` with argument ``multi_label=True``. - - Parameters - ---------- - labels : List[List[int]] - Multi-label classification labels for each example, which is allowed to belong to multiple classes. - The i-th element of `labels` corresponds to list of classes that i-th example belongs to (e.g. ``labels = [[1,2],[1],[0],..]``). - - Important - --------- - *Format requirements*: For dataset with K classes, individual class labels must be integers in 0, 1, ..., K-1. - - pred_probs : np.ndarray - An array of shape ``(N, K)`` of model-predicted probabilities, - ``P(label=k|x)``. Each row of this matrix corresponds - to an example `x` and contains the model-predicted probabilities that - `x` belongs to each possible class, for each of the K classes. The - columns must be ordered such that these probabilities correspond to - class 0, 1, ..., K-1. In multi-label classification, the rows of `pred_probs` need not sum to 1. - - Note - ---- - Estimated label quality scores are most accurate when they are computed based on out-of-sample ``pred_probs`` from your model. - To obtain out-of-sample predicted probabilities for every example in your dataset, you can use :ref:`cross-validation `. - This is encouraged to get better results. - - method : {"self_confidence", "normalized_margin", "confidence_weighted_entropy"}, default = "self_confidence" - Method to calculate separate per class annotation scores that are subsequently aggregated to form an overall label quality score. - These scores are separately calculated for each class based on the corresponding column of `pred_probs` in a one-vs-rest manner, - and are standard label quality scores for multi-class classification. - - See also - -------- - :py:func:`rank.get_label_quality_scores ` function for details about each option. - - adjust_pred_probs : bool, default = False - Account for class imbalance in the label-quality scoring by adjusting predicted probabilities - via subtraction of class confident thresholds and renormalization. - Set this to ``True`` if you prefer to account for class-imbalance. - See `Northcutt et al., 2021 `_. - - aggregator_kwargs : dict, default = {"method": "exponential_moving_average", "alpha": 0.8} - A dictionary of hyperparameter values for aggregating per class scores into an overall label quality score for each example. - Options for ``"method"`` include: ``"exponential_moving_average"`` or ``"softmin"`` or your own callable function. - See :py:class:`internal.multilabel_scorer.Aggregator ` for details about each option and other possible hyperparameters. - - Returns - ------- - label_quality_scores : np.ndarray - A 1D array of shape ``(N,)`` with a label quality score (between 0 and 1) for each example in the dataset. - Lower scores indicate examples whose label is more likely to contain annotation errors. - - - Examples - -------- - >>> from cleanlab.multilabel_classification import get_label_quality_scores - >>> import numpy as np - >>> labels = [[1], [0,2]] - >>> pred_probs = np.array([[0.1, 0.9, 0.1], [0.4, 0.1, 0.9]]) - >>> scores = get_label_quality_scores(labels, pred_probs) - >>> scores - array([0.9, 0.5]) - """ - - assert_valid_inputs( - X=None, y=labels, pred_probs=pred_probs, multi_label=True, allow_one_class=True - ) - num_classes = get_num_classes(labels=labels, pred_probs=pred_probs, multi_label=True) - binary_labels = int2onehot(labels, K=num_classes) - base_scorer = ClassLabelScorer.from_str(method) - base_scorer_kwargs = {"adjust_pred_probs": adjust_pred_probs} - aggregator = Aggregator(**aggregator_kwargs) - scorer = MultilabelScorer(base_scorer, aggregator) - return scorer(binary_labels, pred_probs, base_scorer_kwargs=base_scorer_kwargs) diff --git a/cleanlab/multilabel_classification/__init__.py b/cleanlab/multilabel_classification/__init__.py new file mode 100644 index 0000000000..fcddecdb5d --- /dev/null +++ b/cleanlab/multilabel_classification/__init__.py @@ -0,0 +1,4 @@ +from .rank import get_label_quality_scores +from . import rank +from . import dataset +from . import filter diff --git a/cleanlab/multilabel_classification/dataset.py b/cleanlab/multilabel_classification/dataset.py new file mode 100644 index 0000000000..1f040a98bc --- /dev/null +++ b/cleanlab/multilabel_classification/dataset.py @@ -0,0 +1,342 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . + +""" +Methods to summarize overall labeling issues across a multi-label classification dataset. +Here each example can belong to one or more classes, or none of the classes at all. +Unlike in standard multi-class classification, model-predicted class probabilities need not sum to 1 for each row in multi-label classification. +""" + +import pandas as pd +import numpy as np +from typing import Optional, cast, Dict, Any # noqa: F401 +from cleanlab.multilabel_classification.filter import ( + find_multilabel_issues_per_class, + find_label_issues, +) +from cleanlab.internal.multilabel_utils import get_onehot_num_classes +from collections import defaultdict + + +def common_multilabel_issues( + labels=list, + pred_probs=None, + *, + class_names=None, + confident_joint=None, +) -> pd.DataFrame: + """Summarizes which classes in a multi-label dataset appear most often mislabeled overall. + + Since classes are not mutually exclusive in multi-label classification, this method summarizes the label issues for each class independently of the others. + + Parameters + ---------- + labels : List[List[int]] + List of noisy labels for multi-label classification where each example can belong to multiple classes. + Refer to documentation for this argument in :py:func:`multilabel_classification.filter.find_label_issues ` for further details. + + pred_probs : np.ndarray + An array of shape ``(N, K)`` of model-predicted class probabilities. + Refer to documentation for this argument in :py:func:`multilabel_classification.filter.find_label_issues ` for further details. + + class_names : Iterable[str], optional + A list or other iterable of the string class names. Its order must match the label indices. + If class 0 is 'dog' and class 1 is 'cat', then ``class_names = ['dog', 'cat']``. + If provided, the returned DataFrame will have an extra *Class Name* column with this info. + + confident_joint : np.ndarray, optional + An array of shape ``(K, 2, 2)`` representing a one-vs-rest formatted confident joint. + Refer to documentation for this argument in :py:func:`multilabel_classification.filter.find_label_issues ` for details. + + Returns + ------- + common_multilabel_issues : pd.DataFrame + DataFrame where each row corresponds to a class summarized by the following columns: + - *Class Name*: The name of the class if class_names is provided. + - *Class Index*: The index of the class. + - *In Given Label*: Whether the Class is originally annotated True or False in the given label. + - *In Suggested Label*: Whether the Class should be True or False in the suggested label (based on model's prediction). + - *Num Examples*: Number of examples flagged as a label issue where this Class is True/False "In Given Label" but cleanlab estimates the annotation should actually be as specified "In Suggested Label". I.e. the number of examples in your dataset where this Class was labeled as True but likely should have been False (or vice versa). + - *Issue Probability*: The *Num Examples* column divided by the total number of examples in the dataset; i.e. the relative overall frequency of each type of label issue in your dataset. + + By default, the rows in this DataFrame are ordered by "Issue Probability" (descending). + """ + + num_examples = _get_num_examples_multilabel(labels=labels, confident_joint=confident_joint) + summary_issue_counts = defaultdict(list) + y_one, num_classes = get_onehot_num_classes(labels, pred_probs) + label_issues_list, labels_list, pred_probs_list = find_multilabel_issues_per_class( + labels=labels, + pred_probs=pred_probs, + confident_joint=confident_joint, + return_indices_ranked_by="self_confidence", + ) + + for class_num, (label, issues_for_class) in enumerate(zip(y_one.T, label_issues_list)): + binary_label_issues = np.zeros(len(label)).astype(bool) + binary_label_issues[issues_for_class] = True + true_but_false_count = sum(np.logical_and(label == 1, binary_label_issues)) + false_but_true_count = sum(np.logical_and(label == 0, binary_label_issues)) + + if class_names is not None: + summary_issue_counts["Class Name"].append(class_names[class_num]) + summary_issue_counts["Class Index"].append(class_num) + summary_issue_counts["In Given Label"].append(True) + summary_issue_counts["In Suggested Label"].append(False) + summary_issue_counts["Num Examples"].append(true_but_false_count) + summary_issue_counts["Issue Probability"].append(true_but_false_count / num_examples) + + if class_names is not None: + summary_issue_counts["Class Name"].append(class_names[class_num]) + summary_issue_counts["Class Index"].append(class_num) + summary_issue_counts["In Given Label"].append(False) + summary_issue_counts["In Suggested Label"].append(True) + summary_issue_counts["Num Examples"].append(false_but_true_count) + summary_issue_counts["Issue Probability"].append(false_but_true_count / num_examples) + return ( + pd.DataFrame.from_dict(summary_issue_counts) + .sort_values(by=["Issue Probability"], ascending=False) + .reset_index(drop=True) + ) + + +def rank_classes_by_multilabel_quality( + labels=None, + pred_probs=None, + *, + class_names=None, + joint=None, + confident_joint=None, +) -> pd.DataFrame: + """ + Returns a DataFrame with three overall label quality scores per class for a multi-label dataset. + + These numbers summarize all examples annotated with the class (details listed below under the Returns parameter). + By default, classes are ordered by "Label Quality Score", so the most problematic classes are reported first in the DataFrame. + + Score values are unnormalized and may be very small. What matters is their relative ranking across the classes. + + **Parameters**: + + For information about the arguments to this method, see the documentation of + :py:func:`common_multilabel_issues `. + + Returns + ------- + overall_label_quality : pd.DataFrame + Pandas DataFrame with one row per class and columns: "Class Index", "Label Issues", + "Inverse Label Issues", "Label Issues", "Inverse Label Noise", "Label Quality Score". + Some entries are overall quality scores between 0 and 1, summarizing how good overall the labels + appear to be for that class (lower values indicate more erroneous labels). + Other entries are estimated counts of annotation errors related to this class. + + Here is what each column represents: + - *Class Name*: The name of the class if class_names is provided. + - *Class Index*: The index of the class in 0, 1, ..., K-1. + - *Label Issues*: Estimated number of examples in the dataset that are labeled as belonging to class k but actually should not belong to this class. + - *Inverse Label Issues*: Estimated number of examples in the dataset that should actually be labeled as class k but did not receive this label. + - *Label Noise*: Estimated proportion of examples in the dataset that are labeled as class k but should not be. For each class k: this is computed by dividing the number of examples with "Label Issues" that were labeled as class k by the total number of examples labeled as class k. + - *Inverse Label Noise*: Estimated proportion of examples in the dataset that should actually be labeled as class k but did not receive this label. + - *Label Quality Score*: Estimated proportion of examples labeled as class k that have been labeled correctly, i.e. ``1 - label_noise``. + + By default, the DataFrame is ordered by "Label Quality Score" (in ascending order), so the classes with the most label issues appear first. + """ + + issues_df = common_multilabel_issues( + labels=labels, pred_probs=pred_probs, class_names=class_names, confident_joint=joint + ) + issues_dict = defaultdict(defaultdict) # type: Dict[str, Any] + num_examples = _get_num_examples_multilabel(labels=labels, confident_joint=confident_joint) + return_columns = [ + "Class Name", + "Class Index", + "Label Issues", + "Inverse Label Issues", + "Label Noise", + "Inverse Label Noise", + "Label Quality Score", + ] + if class_names is None: + return_columns = return_columns[1:] + for class_num, row in issues_df.iterrows(): + if row["In Given Label"]: + if class_names is not None: + issues_dict[row["Class Index"]]["Class Name"] = row["Class Name"] + issues_dict[row["Class Index"]]["Label Issues"] = int( + row["Issue Probability"] * num_examples + ) + issues_dict[row["Class Index"]]["Label Noise"] = row["Issue Probability"] + issues_dict[row["Class Index"]]["Label Quality Score"] = ( + 1 - issues_dict[row["Class Index"]]["Label Noise"] + ) + else: + if class_names is not None: + issues_dict[row["Class Index"]]["Class Name"] = row["Class Name"] + issues_dict[row["Class Index"]]["Inverse Label Issues"] = int( + row["Issue Probability"] * num_examples + ) + issues_dict[row["Class Index"]]["Inverse Label Noise"] = row["Issue Probability"] + + issues_df_dict = defaultdict(list) + for i in issues_dict: + issues_df_dict["Class Index"].append(i) + for j in issues_dict[i]: + issues_df_dict[j].append(issues_dict[i][j]) + return ( + pd.DataFrame.from_dict(issues_df_dict) + .sort_values(by="Label Quality Score", ascending=True) + .reset_index(drop=True) + )[return_columns] + + +def _get_num_examples_multilabel(labels=None, confident_joint: Optional[np.ndarray] = None) -> int: + """Helper method that finds the number of examples from the parameters or throws an error + if neither parameter is provided. + + Parameters + ---------- + For parameter info, see the docstring of :py:func:`common_multilabel_issues `. + + Returns + ------- + num_examples : int + The number of examples in the dataset. + + Raises + ------ + ValueError + If `labels` is None. + """ + + if labels is None and confident_joint is None: + raise ValueError( + "Error: num_examples is None. You must either provide confident_joint, " + "or provide both num_example and joint as input parameters." + ) + _confident_joint = cast(np.ndarray, confident_joint) + num_examples = len(labels) if labels is not None else cast(int, np.sum(_confident_joint[0])) + return num_examples + + +def overall_multilabel_health_score( + labels=None, + pred_probs=None, + *, + confident_joint=None, +) -> float: + """Returns a single score between 0 and 1 measuring the overall quality of all labels in a multi-label classification dataset. + Intuitively, the score is the average correctness of the given labels across all examples in the + dataset. So a score of 1 suggests your data is perfectly labeled and a score of 0.5 suggests + half of the examples in the dataset may be incorrectly labeled. Thus, a higher + score implies a higher quality dataset. + + **Parameters**: For information about the arguments to this method, see the documentation of + :py:func:`common_multilabel_issues `. + + Returns + ------- + health_score : float + A overall score between 0 and 1, where 1 implies all labels in the dataset are estimated to be correct. + A score of 0.5 implies that half of the dataset's labels are estimated to have issues. + """ + num_examples = _get_num_examples_multilabel(labels=labels) + issues = find_label_issues( + labels=labels, pred_probs=pred_probs, confident_joint=confident_joint + ) + return 1.0 - sum(issues) / num_examples + + +def multilabel_health_summary( + labels=None, + pred_probs=None, + *, + class_names=None, + num_examples=None, + confident_joint=None, + verbose=True, +) -> Dict: + """Prints a health summary of your multi-label dataset. + + This summary includes useful statistics like: + + * The classes with the most and least label issues. + * Overall label quality scores, summarizing how accurate the labels appear across the entire dataset. + + **Parameters**: For information about the arguments to this method, see the documentation of + :py:func:`common_multilabel_issues `. + + Returns + ------- + summary : dict + A dictionary containing keys (see the corresponding functions' documentation to understand the values): + - ``"overall_label_health_score"``, corresponding to output of :py:func:`overall_multilabel_health_score ` + - ``"classes_by_multilabel_quality"``, corresponding to output of :py:func:`rank_classes_by_multilabel_quality ` + - ``"common_multilabel_issues"``, corresponding to output of :py:func:`common_multilabel_issues ` + """ + from cleanlab.internal.util import smart_display_dataframe + + if num_examples is None: + num_examples = _get_num_examples_multilabel(labels=labels) + + if verbose: + longest_line = f"| for your dataset with {num_examples:,} examples " + print( + "-" * (len(longest_line) - 1) + + "\n" + + f"| Generating a Cleanlab Dataset Health Summary{' ' * (len(longest_line) - 49)}|\n" + + longest_line + + f"| Note, Cleanlab is not a medical doctor... yet.{' ' * (len(longest_line) - 51)}|\n" + + "-" * (len(longest_line) - 1) + + "\n", + ) + + df_class_label_quality = rank_classes_by_multilabel_quality( + labels=labels, + pred_probs=pred_probs, + class_names=class_names, + confident_joint=confident_joint, + ) + if verbose: + print("Overall Class Quality and Noise across your dataset (below)") + print("-" * 60, "\n", flush=True) + smart_display_dataframe(df_class_label_quality) + + df_common_issues = common_multilabel_issues( + labels=labels, + pred_probs=pred_probs, + class_names=class_names, + confident_joint=confident_joint, + ) + if verbose: + print( + "\nCommon multilabel issues are" + "\n" + "-" * 83 + "\n", + flush=True, + ) + smart_display_dataframe(df_common_issues) + print() + + health_score = overall_multilabel_health_score( + labels=labels, + pred_probs=pred_probs, + confident_joint=confident_joint, + ) + if verbose: + print("\nGenerated with <3 from Cleanlab.\n") + return { + "overall_multilabel_health_score": health_score, + "classes_by_multilabel_quality": df_class_label_quality, + "common_multilabel_issues": df_common_issues, + } diff --git a/cleanlab/multilabel_classification/filter.py b/cleanlab/multilabel_classification/filter.py new file mode 100644 index 0000000000..bbddc065dc --- /dev/null +++ b/cleanlab/multilabel_classification/filter.py @@ -0,0 +1,275 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . + +""" +Methods to flag which examples have label issues in multi-label classification datasets. +Here each example can belong to one or more classes, or none of the classes at all. +Unlike in standard multi-class classification, model-predicted class probabilities need not sum to 1 for each row in multi-label classification. +""" + +import warnings +from typing import Optional, Union, Tuple, List, Any +import numpy as np + + +def find_label_issues( + labels: list, + pred_probs: np.ndarray, + return_indices_ranked_by: Optional[str] = None, + rank_by_kwargs={}, + filter_by: str = "prune_by_noise_rate", + frac_noise: float = 1.0, + num_to_remove_per_class: Optional[List[int]] = None, + min_examples_per_class=1, + confident_joint: Optional[np.ndarray] = None, + n_jobs: Optional[int] = None, + verbose: bool = False, +) -> np.ndarray: + """ + Identifies potentially mislabeled examples in a multi-label classification dataset. + An example is flagged as with a label issue if *any* of the classes appear to be incorrectly annotated for this example. + + Parameters + ---------- + labels : List[List[int]] + List of noisy labels for multi-label classification where each example can belong to multiple classes. + This is an iterable of iterables where the i-th element of `labels` corresponds to a list of classes that the i-th example belongs to, + according to the original data annotation (e.g. ``labels = [[1,2],[1],[0],..]``). + This method will return the indices i where the inner list ``labels[i]`` is estimated to have some error. + For a dataset with K classes, each class must be represented as an integer in 0, 1, ..., K-1 within the labels. + + pred_probs : np.ndarray + An array of shape ``(N, K)`` of model-predicted class probabilities. + Each row of this matrix corresponds to an example `x` + and contains the predicted probability that `x` belongs to each possible class, + for each of the K classes (along its columns). + The columns need not sum to 1 but must be ordered such that + these probabilities correspond to class 0, 1, ..., K-1. + + Note + ---- + Estimated label quality scores are most accurate when they are computed based on out-of-sample ``pred_probs`` from your model. + To obtain out-of-sample predicted probabilities for every example in your dataset, you can use :ref:`cross-validation `. + This is encouraged to get better results. + + return_indices_ranked_by : {None, 'self_confidence', 'normalized_margin', 'confidence_weighted_entropy'}, default = None + This function can return a boolean mask (if None) or an array of the example-indices with issues sorted based on the specified ranking method. + Refer to documentation for this argument in :py:func:`filter.find_label_issues ` for details. + + rank_by_kwargs : dict, optional + Optional keyword arguments to pass into scoring functions for ranking by + label quality score (see :py:func:`rank.get_label_quality_scores + `). + + filter_by : {'prune_by_class', 'prune_by_noise_rate', 'both', 'confident_learning', 'predicted_neq_given', 'low_normalized_margin', 'low_self_confidence'}, default='prune_by_noise_rate' + The specific Confident Learning method to determine precisely which examples have label issues in a dataset. + Refer to documentation for this argument in :py:func:`filter.find_label_issues ` for details. + + frac_noise : float, default = 1.0 + This will return the "top" frac_noise * num_label_issues estimated label errors, dependent on the filtering method used, + Refer to documentation for this argument in :py:func:`filter.find_label_issues ` for details. + + num_to_remove_per_class : array_like + An iterable that specifies the number of mislabeled examples to return from each class. + Refer to documentation for this argument in :py:func:`filter.find_label_issues ` for details. + + min_examples_per_class : int, default = 1 + The minimum number of examples required per class below which examples from this class will not be flagged as label issues. + Refer to documentation for this argument in :py:func:`filter.find_label_issues ` for details. + + confident_joint : np.ndarray, optional + An array of shape ``(K, 2, 2)`` representing a one-vs-rest formatted confident joint, as is appropriate for multi-label classification tasks. + Entry ``(c, i, j)`` in this array is the number of examples confidently counted into a ``(class c, noisy label=i, true label=j)`` bin, + where `i, j` are either 0 or 1 to denote whether this example belongs to class `c` or not + (recall examples can belong to multiple classes in multi-label classification). + The `confident_joint` can be computed using :py:func:`count.compute_confident_joint ` with ``multi_label=True``. + If not provided, it is computed from the given (noisy) `labels` and `pred_probs`. + + n_jobs : optional + Number of processing threads used by multiprocessing. + Refer to documentation for this argument in :py:func:`filter.find_label_issues ` for details. + + verbose : optional + If ``True``, prints when multiprocessing happens. + + Returns + ------- + label_issues : np.ndarray + If `return_indices_ranked_by` left unspecified, returns a boolean **mask** for the entire dataset + where ``True`` represents an example suffering from some label issue and + ``False`` represents an example that appears accurately labeled. + + If `return_indices_ranked_by` is specified, this method instead returns a list of **indices** of examples identified with + label issues (i.e. those indices where the mask would be ``True``). + Indices are sorted by the likelihood that *all* classes are correctly annotated for the corresponding example. + + Note + ---- + Obtain the *indices* of examples with label issues in your dataset by setting + `return_indices_ranked_by`. + + """ + from cleanlab.filter import _find_label_issues_multilabel + + return _find_label_issues_multilabel( + labels=labels, + pred_probs=pred_probs, + return_indices_ranked_by=return_indices_ranked_by, + rank_by_kwargs=rank_by_kwargs, + filter_by=filter_by, + frac_noise=frac_noise, + num_to_remove_per_class=num_to_remove_per_class, + min_examples_per_class=min_examples_per_class, + confident_joint=confident_joint, + n_jobs=n_jobs, + verbose=verbose, + ) + + +def find_multilabel_issues_per_class( + labels: list, + pred_probs: np.ndarray, + return_indices_ranked_by: Optional[str] = None, + rank_by_kwargs={}, + filter_by: str = "prune_by_noise_rate", + frac_noise: float = 1.0, + num_to_remove_per_class: Optional[List[int]] = None, + min_examples_per_class=1, + confident_joint: Optional[np.ndarray] = None, + n_jobs: Optional[int] = None, + verbose: bool = False, +) -> Union[np.ndarray, Tuple[List[np.ndarray], List[Any], List[np.ndarray]]]: + """ + Identifies potentially bad labels for each example and each class in a multi-label classification dataset. + Whereas :py:func:`find_label_issues ` + estimates which examples have an erroneous annotation for *any* class, this method estimates which specific classes are incorrectly annotated as well. + This method returns a list of size K, the number of classes in the dataset. + + Parameters + ---------- + labels : List[List[int]] + List of noisy labels for multi-label classification where each example can belong to multiple classes. + Refer to documentation for this argument in :py:func:`find_label_issues ` for further details. + This method will identify whether ``labels[i][k]`` appears correct, for every example ``i`` and class ``k``. + + pred_probs : np.ndarray + An array of shape ``(N, K)`` of model-predicted class probabilities. + Refer to documentation for this argument in :py:func:`find_label_issues ` for further details. + + return_indices_ranked_by : {None, 'self_confidence', 'normalized_margin', 'confidence_weighted_entropy'}, default = None + This function can return a boolean mask (if this argument is ``None``) or a sorted array of indices based on the specified ranking method (if not ``None``). + Refer to documentation for this argument in :py:func:`filter.find_label_issues ` for details. + + rank_by_kwargs : dict, optional + Optional keyword arguments to pass into scoring functions for ranking by. + label quality score (see :py:func:`rank.get_label_quality_scores + `). + + filter_by : {'prune_by_class', 'prune_by_noise_rate', 'both', 'confident_learning', 'predicted_neq_given', 'low_normalized_margin', 'low_self_confidence'}, default = 'prune_by_noise_rate' + The specific method that can be used to filter or prune examples with label issues from a dataset. + Refer to documentation for this argument in :py:func:`filter.find_label_issues ` for details. + + frac_noise : float, default = 1.0 + This will return the "top" frac_noise * num_label_issues estimated label errors, dependent on the filtering method used, + Refer to documentation for this argument in :py:func:`filter.find_label_issues ` for details. + + num_to_remove_per_class : array_like + This parameter is an iterable that specifies the number of mislabeled examples to return from each class. + Refer to documentation for this argument in :py:func:`filter.find_label_issues ` for details. + + min_examples_per_class : int, default = 1 + The minimum number of examples required per class to avoid flagging as label issues. + Refer to documentation for this argument in :py:func:`filter.find_label_issues ` for details. + + confident_joint : np.ndarray, optional + An array of shape ``(K, 2, 2)`` representing a one-vs-rest formatted confident joint. + Refer to documentation for this argument in :py:func:`cleanlab.multilabel_classification.filter.find_label_issues ` for details. + + n_jobs : optional + Number of processing threads used by multiprocessing. + Refer to documentation for this argument in :py:func:`filter.find_label_issues ` for details. + + verbose : optional + If ``True``, prints when multiprocessing happens. + + Returns + ------- + per_class_label_issues : list(np.ndarray) + By default, this is a list of length K containing the examples where each class appears incorrectly annotated. + ``per_class_label_issues[k]`` is a Boolean mask of the same length as the dataset, + where ``True`` values indicate examples where class ``k`` appears incorrectly annotated. + + For more details, refer to :py:func:`cleanlab.multilabel_classification.filter.find_label_issues `. + + Otherwise if `return_indices_ranked_by` is not ``None``, then this method returns 3 objects (each of length K, the number of classes): `label_issues_list`, `labels_list`, `pred_probs_list`. + - *label_issues_list*: an ordered list of indices of examples where class k appears incorrectly annotated, sorted by the likelihood that class k is correctly annotated. + - *labels_list*: a binary one-hot representation of the original labels, useful if you want to compute label quality scores. + - *pred_probs_list*: a one-vs-rest representation of the original predicted probabilities of shape ``(N, 2)``, useful if you want to compute label quality scores. + ``pred_probs_list[k][i][0]`` is the estimated probability that example ``i`` belongs to class ``k``, and is equal to: ``1 - pred_probs_list[k][i][1]``. + """ + import cleanlab.filter + from cleanlab.internal.multilabel_utils import get_onehot_num_classes, stack_complement + + y_one, num_classes = get_onehot_num_classes(labels, pred_probs) + if return_indices_ranked_by is None: + bissues = np.zeros(y_one.shape).astype(bool) + else: + label_issues_list = [] + labels_list = [] + pred_probs_list = [] + if confident_joint is not None: + confident_joint_shape = confident_joint.shape + if confident_joint_shape == (num_classes, num_classes): + warnings.warn( + f"The new recommended format for `confident_joint` in multi_label settings is (num_classes,2,2) as output by compute_confident_joint(...,multi_label=True). Your K x K confident_joint in the old format is being ignored." + ) + confident_joint = None + elif confident_joint_shape != (num_classes, 2, 2): + raise ValueError("confident_joint should be of shape (num_classes, 2, 2)") + for class_num, (label, pred_prob_for_class) in enumerate(zip(y_one.T, pred_probs.T)): + pred_probs_binary = stack_complement(pred_prob_for_class) + if confident_joint is None: + conf = None + else: + conf = confident_joint[class_num] + if num_to_remove_per_class is not None: + ml_num_to_remove_per_class = [num_to_remove_per_class[class_num], 0] + else: + ml_num_to_remove_per_class = None + binary_label_issues = cleanlab.filter.find_label_issues( + labels=label, + pred_probs=pred_probs_binary, + return_indices_ranked_by=return_indices_ranked_by, + frac_noise=frac_noise, + rank_by_kwargs=rank_by_kwargs, + filter_by=filter_by, + num_to_remove_per_class=ml_num_to_remove_per_class, + min_examples_per_class=min_examples_per_class, + confident_joint=conf, + n_jobs=n_jobs, + verbose=verbose, + ) + + if return_indices_ranked_by is None: + bissues[:, class_num] = binary_label_issues + else: + label_issues_list.append(binary_label_issues) + labels_list.append(label) + pred_probs_list.append(pred_probs_binary) + if return_indices_ranked_by is None: + return bissues + else: + return label_issues_list, labels_list, pred_probs_list diff --git a/cleanlab/multilabel_classification/rank.py b/cleanlab/multilabel_classification/rank.py new file mode 100644 index 0000000000..75eb7960fe --- /dev/null +++ b/cleanlab/multilabel_classification/rank.py @@ -0,0 +1,194 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . + +""" +Methods to rank the severity of label issues in multi-label classification datasets. +Here each example can belong to one or more classes, or none of the classes at all. +Unlike in standard multi-class classification, model-predicted class probabilities need not sum to 1 for each row in multi-label classification. +""" +from __future__ import annotations + +import numpy as np # noqa: F401: Imported for type annotations +from typing import List, TypeVar, Dict, Any, Optional, Tuple, TYPE_CHECKING + +from cleanlab.internal.validation import assert_valid_inputs +from cleanlab.internal.util import get_num_classes +from cleanlab.internal.multilabel_utils import int2onehot +from cleanlab.internal.multilabel_scorer import MultilabelScorer, ClassLabelScorer, Aggregator + + +if TYPE_CHECKING: # pragma: no cover + import numpy.typing as npt + + T = TypeVar("T", bound=npt.NBitBase) + + +def _labels_to_binary( + labels: List[List[int]], + pred_probs: npt.NDArray["np.floating[T]"], +) -> np.ndarray: + """Validate the inputs to the multilabel scorer. Also transform the labels to a binary representation.""" + assert_valid_inputs( + X=None, y=labels, pred_probs=pred_probs, multi_label=True, allow_one_class=True + ) + num_classes = get_num_classes(labels=labels, pred_probs=pred_probs, multi_label=True) + binary_labels = int2onehot(labels, K=num_classes) + return binary_labels + + +def _create_multilabel_scorer( + method: str, + adjust_pred_probs: bool, + aggregator_kwargs: Optional[Dict[str, Any]] = None, +) -> Tuple[MultilabelScorer, Dict]: + """This function acts as a factory that creates a MultilabelScorer.""" + base_scorer = ClassLabelScorer.from_str(method) + base_scorer_kwargs = {"adjust_pred_probs": adjust_pred_probs} + if aggregator_kwargs: + aggregator = Aggregator(**aggregator_kwargs) + scorer = MultilabelScorer(base_scorer, aggregator) + else: + scorer = MultilabelScorer(base_scorer) + return scorer, base_scorer_kwargs + + +def get_label_quality_scores( + labels: List[List[int]], + pred_probs: npt.NDArray["np.floating[T]"], + *, + method: str = "self_confidence", + adjust_pred_probs: bool = False, + aggregator_kwargs: Dict[str, Any] = {"method": "exponential_moving_average", "alpha": 0.8}, +) -> npt.NDArray["np.floating[T]"]: + """Computes a label quality score for each example in a multi-label classification dataset. + + Scores are between 0 and 1 with lower scores indicating examples whose label more likely contains an error. + For each example, this method internally computes a separate score for each individual class + and then aggregates these per-class scores into an overall label quality score for the example. + + + Parameters + ---------- + labels : List[List[int]] + List of noisy labels for multi-label classification where each example can belong to multiple classes. + Refer to documentation for this argument in :py:func:`multilabel_classification.filter.find_label_issues ` for further details. + + pred_probs : np.ndarray + An array of shape ``(N, K)`` of model-predicted class probabilities. + Refer to documentation for this argument in :py:func:`multilabel_classification.filter.find_label_issues ` for further details. + + method : {"self_confidence", "normalized_margin", "confidence_weighted_entropy"}, default = "self_confidence" + Method to calculate separate per-class annotation scores for an example that are then aggregated into an overall label quality score for the example. + These scores are separately calculated for each class based on the corresponding column of `pred_probs` in a one-vs-rest manner, + and are standard label quality scores for binary classification (based on whether the class should or should not apply to this example). + + See also + -------- + :py:func:`rank.get_label_quality_scores ` function for details about each option. + + adjust_pred_probs : bool, default = False + Account for class imbalance in the label-quality scoring by adjusting predicted probabilities. + Refer to documentation for this argument in :py:func:`rank.get_label_quality_scores ` for details. + + + aggregator_kwargs : dict, default = {"method": "exponential_moving_average", "alpha": 0.8} + A dictionary of hyperparameter values to use when aggregating per-class scores into an overall label quality score for each example. + Options for ``"method"`` include: ``"exponential_moving_average"`` or ``"softmin"`` or your own callable function. + See :py:class:`internal.multilabel_scorer.Aggregator ` for details about each option and other possible hyperparameters. + + To get a score for each class annotation for each example, use the :py:func:`multilabel_classification.classification.rank.get_label_quality_scores_per_class ` method instead. + + Returns + ------- + label_quality_scores : np.ndarray + A 1D array of shape ``(N,)`` with a label quality score (between 0 and 1) for each example in the dataset. + Lower scores indicate examples whose label is more likely to contain some annotation error (for any of the classes). + + Examples + -------- + >>> from cleanlab.multilabel_classification import get_label_quality_scores + >>> import numpy as np + >>> labels = [[1], [0,2]] + >>> pred_probs = np.array([[0.1, 0.9, 0.1], [0.4, 0.1, 0.9]]) + >>> scores = get_label_quality_scores(labels, pred_probs) + >>> scores + array([0.9, 0.5]) + """ + binary_labels = _labels_to_binary(labels, pred_probs) + scorer, base_scorer_kwargs = _create_multilabel_scorer( + method=method, + adjust_pred_probs=adjust_pred_probs, + aggregator_kwargs=aggregator_kwargs, + ) + return scorer(binary_labels, pred_probs, base_scorer_kwargs=base_scorer_kwargs) + + +def get_label_quality_scores_per_class( + labels: List[List[int]], + pred_probs: npt.NDArray["np.floating[T]"], + *, + method: str = "self_confidence", + adjust_pred_probs: bool = False, +) -> np.ndarray: + """ + Computes a quality score quantifying how likely each individual class annotation is correct in a multi-label classification dataset. + This is similar to :py:func:`get_label_quality_scores ` + but instead returns the per-class results without aggregation. + For a dataset with K classes, each example receives K scores from this method. + Refer to documentation in :py:func:`get_label_quality_scores ` for details. + + Parameters + ---------- + labels : List[List[int]] + List of noisy labels for multi-label classification where each example can belong to multiple classes. + Refer to documentation for this argument in :py:func:`find_label_issues ` for further details. + + pred_probs : np.ndarray + An array of shape ``(N, K)`` of model-predicted class probabilities. + Refer to documentation for this argument in :py:func:`find_label_issues ` for further details. + + method : {"self_confidence", "normalized_margin", "confidence_weighted_entropy"}, default = "self_confidence" + Method to calculate separate per-class annotation scores (that quantify how likely a particular class annotation is correct for a particular example). + Refer to documentation for this argument in :py:func:`get_label_quality_scores ` for further details. + + adjust_pred_probs : bool, default = False + Account for class imbalance in the label-quality scoring by adjusting predicted probabilities. + Refer to documentation for this argument in :py:func:`rank.get_label_quality_scores ` for details. + + Returns + ------- + label_quality_scores : list(np.ndarray) + A list containing K arrays, each of shape (N,). Here K is the number of classes in the dataset and N is the number of examples. + ``label_quality_scores[k][i]`` is a score between 0 and 1 quantifying how likely the annotation for class ``k`` is correct for example ``i``. + + Examples + -------- + >>> from cleanlab.multilabel_classification import get_label_quality_scores + >>> import numpy as np + >>> labels = [[1], [0,2]] + >>> pred_probs = np.array([[0.1, 0.9, 0.1], [0.4, 0.1, 0.9]]) + >>> scores = get_label_quality_scores(labels, pred_probs) + >>> scores + array([0.9, 0.5]) + """ + binary_labels = _labels_to_binary(labels, pred_probs) + scorer, base_scorer_kwargs = _create_multilabel_scorer( + method=method, + adjust_pred_probs=adjust_pred_probs, + ) + return scorer.get_class_label_quality_scores( + labels=binary_labels, pred_probs=pred_probs, base_scorer_kwargs=base_scorer_kwargs + ) diff --git a/cleanlab/object_detection/__init__.py b/cleanlab/object_detection/__init__.py new file mode 100644 index 0000000000..fbc2eb7eac --- /dev/null +++ b/cleanlab/object_detection/__init__.py @@ -0,0 +1,3 @@ +from . import rank +from . import filter +from . import summary diff --git a/cleanlab/object_detection/filter.py b/cleanlab/object_detection/filter.py new file mode 100644 index 0000000000..cf51f66214 --- /dev/null +++ b/cleanlab/object_detection/filter.py @@ -0,0 +1,211 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . + +"""Methods to find label issues in an object detection dataset, where each annotated bounding box in an image receives its own class label.""" + +from typing import List, Any, Dict +import numpy as np + +from cleanlab.internal.constants import ( + ALPHA, + LOW_PROBABILITY_THRESHOLD, + HIGH_PROBABILITY_THRESHOLD, + OVERLOOKED_THRESHOLD, + BADLOC_THRESHOLD, + SWAP_THRESHOLD, +) +from cleanlab.internal.object_detection_utils import assert_valid_inputs + +from cleanlab.object_detection.rank import ( + _get_valid_inputs_for_compute_scores, + compute_overlooked_box_scores, + compute_badloc_box_scores, + compute_swap_box_scores, + get_label_quality_scores, + issues_from_scores, +) + + +def find_label_issues( + labels: List[Dict[str, Any]], + predictions: List[np.ndarray], + *, + return_indices_ranked_by_score: bool = False, +) -> np.ndarray: + """ + Identifies potentially mislabeled images in an object detection dataset. + An image is flagged with a label issue if *any* of its bounding boxes appear incorrectly annotated. + This includes images for which a bounding box: should have been annotated but is missing, + has been annotated with the wrong class, or has been annotated in a suboptimal location. + + Suppose the dataset has ``N`` images, ``K`` possible class labels. + If ``return_indices_ranked_by_score`` is ``False``, a boolean mask of length ``N`` is returned, + indicating whether each image has a label issue (``True``) or not (``False``). + If ``return_indices_ranked_by_score`` is ``True``, the indices of images flagged with label issues are returned, + sorted with the most likely-mislabeled images ordered first. + + Parameters + ---------- + labels: + Annotated boxes and class labels in the original dataset, which may contain some errors. + This is a list of ``N`` dictionaries such that ``labels[i]`` contains the given labels for the `i`-th image in the following format: + ``{'bboxes': np.ndarray((L,4)), 'labels': np.ndarray((L,)), 'image_name': str}`` where ``L`` is the number of annotated bounding boxes + for the `i`-th image and ``bboxes[l]`` is a bounding box of coordinates in ``[x1,y1,x2,y2]`` format with given class label ``labels[j]``. + ``image_name`` is an optional part of the labels that can be used to later refer to specific images. + + For more information on proper labels formatting, check out the `MMDetection library `_. + + predictions: + Predictions output by a trained object detection model. + For the most accurate results, predictions should be out-of-sample to avoid overfitting, eg. obtained via :ref:`cross-validation `. + This is a list of ``N`` ``np.ndarray`` such that ``predictions[i]`` corresponds to the model prediction for the `i`-th image. + For each possible class ``k`` in 0, 1, ..., K-1: ``predictions[i][k]`` is a ``np.ndarray`` of shape ``(M,5)``, + where ``M`` is the number of predicted bounding boxes for class ``k``. Here the five columns correspond to ``[x1,y1,x2,y2,pred_prob]``, + where ``[x1,y1,x2,y2]`` are coordinates of the bounding box predicted by the model + and ``pred_prob`` is the model's confidence in the predicted class label for this bounding box. + + Note: Here, ``[x1,y1]`` corresponds to the coordinates of the bottom-left corner of the bounding box, while ``[x2,y2]`` corresponds to the coordinates of the top-right corner of the bounding box. The last column, pred_prob, represents the predicted probability that the bounding box contains an object of the class k. + + For more information see the `MMDetection package `_ for an example object detection library that outputs predictions in the correct format. + + return_indices_ranked_by_score: + Determines what is returned by this method (see description of return value for details). + + Returns + ------- + label_issues : np.ndarray + Specifies which images are identified to have a label issue. + If ``return_indices_ranked_by_score = False``, this function returns a boolean mask of length ``N`` (``True`` entries indicate which images have label issue). + If ``return_indices_ranked_by_score = True``, this function returns a (shorter) array of indices of images with label issues, sorted by how likely the image is mislabeled. + + More precisely, indices are sorted by image label quality score calculated via :py:func:`object_detection.rank.get_label_quality_scores `. + """ + scoring_method = "objectlab" + + assert_valid_inputs( + labels=labels, + predictions=predictions, + method=scoring_method, + ) + + is_issue = _find_label_issues( + labels, + predictions, + scoring_method=scoring_method, + return_indices_ranked_by_score=return_indices_ranked_by_score, + ) + + return is_issue + + +def _find_label_issues( + labels: List[Dict[str, Any]], + predictions: List[np.ndarray], + *, + return_indices_ranked_by_score: bool = True, + scoring_method: str = "objectlab", +): + """Internal function to find label issues based on passed in method.""" + + if scoring_method == "objectlab": + auxiliary_inputs = _get_valid_inputs_for_compute_scores(ALPHA, labels, predictions) + + overlooked_scores_per_box = compute_overlooked_box_scores( + alpha=ALPHA, + high_probability_threshold=HIGH_PROBABILITY_THRESHOLD, + auxiliary_inputs=auxiliary_inputs, + ) + overlooked_issues_per_box = _find_label_issues_per_box( + overlooked_scores_per_box, OVERLOOKED_THRESHOLD + ) + overlooked_issues_per_image = _pool_box_scores_per_image(overlooked_issues_per_box) + + badloc_scores_per_box = compute_badloc_box_scores( + alpha=ALPHA, + low_probability_threshold=LOW_PROBABILITY_THRESHOLD, + auxiliary_inputs=auxiliary_inputs, + ) + badloc_issues_per_box = _find_label_issues_per_box(badloc_scores_per_box, BADLOC_THRESHOLD) + badloc_issues_per_image = _pool_box_scores_per_image(badloc_issues_per_box) + + swap_scores_per_box = compute_swap_box_scores( + alpha=ALPHA, + high_probability_threshold=HIGH_PROBABILITY_THRESHOLD, + auxiliary_inputs=auxiliary_inputs, + ) + swap_issues_per_box = _find_label_issues_per_box(swap_scores_per_box, SWAP_THRESHOLD) + swap_issues_per_image = _pool_box_scores_per_image(swap_issues_per_box) + + issues_per_image = ( + overlooked_issues_per_image + badloc_issues_per_image + swap_issues_per_image + ) + is_issue = issues_per_image > 0 + else: + is_issue = np.full( + shape=[ + len(labels), + ], + fill_value=-1, + ) + + if return_indices_ranked_by_score: + scores = get_label_quality_scores(labels, predictions) + sorted_scores_idx = issues_from_scores(scores, threshold=1.0) + is_issue_idx = np.where(is_issue == True)[0] + sorted_issue_mask = np.in1d(sorted_scores_idx, is_issue_idx, assume_unique=True) + issue_idx = sorted_scores_idx[sorted_issue_mask] + return issue_idx + else: + return is_issue + + +def _find_label_issues_per_box( + scores_per_box: List[np.ndarray], threshold: float +) -> List[np.ndarray]: + """Takes in a list of size ``N`` where each index is an array of scores for each bounding box in the `n-th` example + and a threshold. Each box below or equal to the threshold will be marked as an issue. + + Returns a list of size ``N`` where each index is a boolean array of length number of boxes per example `n` + marking if a specific box is an issue - 1 or not - 0.""" + is_issue_per_box = [] + for idx, score_per_box in enumerate(scores_per_box): + if len(score_per_box) == 0: # if no for specific image, then image not an issue + is_issue_per_box.append(np.array([False])) + else: + score_per_box[np.isnan(score_per_box)] = 1.0 + score_per_box = score_per_box + issue_per_box = score_per_box <= threshold + is_issue_per_box.append(issue_per_box) + return is_issue_per_box + + +def _pool_box_scores_per_image(is_issue_per_box: List[np.ndarray]) -> np.ndarray: + """Takes in a list of size ``N`` where each index is a boolean array of length number of boxes per image `n ` + marking if a specific box is an issue - 1 or not - 0. + + Returns a list of size ``N`` where each index marks if the image contains an issue - 1 or not - 0. + Images are marked as issues if 1 or more bounding boxes in the image is an issue.""" + is_issue = np.zeros( + shape=[ + len( + is_issue_per_box, + ) + ] + ) + for idx, issue_per_box in enumerate(is_issue_per_box): + if np.sum(issue_per_box) > 0: + is_issue[idx] = 1 + return is_issue diff --git a/cleanlab/object_detection/rank.py b/cleanlab/object_detection/rank.py new file mode 100644 index 0000000000..ceca1c0c52 --- /dev/null +++ b/cleanlab/object_detection/rank.py @@ -0,0 +1,1062 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . + +"""Methods to rank and score images in an object detection dataset (object detection data), based on how likely they +are to contain label errors. """ + +import warnings + +from cleanlab.internal.constants import ( + ALPHA, + CUSTOM_SCORE_WEIGHT_BADLOC, + CUSTOM_SCORE_WEIGHT_OVERLOOKED, + CUSTOM_SCORE_WEIGHT_SWAP, + EUC_FACTOR, + HIGH_PROBABILITY_THRESHOLD, + LOW_PROBABILITY_THRESHOLD, + MAX_ALLOWED_BOX_PRUNE, + TINY_VALUE, + TEMPERATURE, +) + + +import copy +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, TypeVar + +import numpy as np +from cleanlab.internal.object_detection_utils import ( + softmin1d, + assert_valid_aggregation_weights, + assert_valid_inputs, +) + +if TYPE_CHECKING: # pragma: no cover + from typing import TypedDict + + AuxiliaryTypesDict = TypedDict( + "AuxiliaryTypesDict", + { + "pred_labels": np.ndarray, + "pred_label_probs": np.ndarray, + "pred_bboxes": np.ndarray, + "lab_labels": np.ndarray, + "lab_bboxes": np.ndarray, + "similarity_matrix": np.ndarray, + "min_possible_similarity": float, + }, + ) +else: + AuxiliaryTypesDict = TypeVar("AuxiliaryTypesDict") + + +def get_label_quality_scores( + labels: List[Dict[str, Any]], + predictions: List[np.ndarray], + aggregation_weights: Optional[Dict[str, float]] = None, + *, + verbose: bool = True, +) -> np.ndarray: + """Computes a label quality score for each image of the ``N`` images in the dataset. + + For object detection datasets, the label quality score for an image estimates how likely it has been correctly labeled. + Lower scores indicate images whose annotation is more likely imperfect. + Annotators may have mislabeled an image because they: + + - overlooked an object (missing annotated bounding box), + - chose the wrong class label for an annotated box in the correct location, + - imperfectly annotated the location/edges of a bounding box. + + Any of these annotation errors should lead to an image with a lower label quality score. This quality score is between 0 and 1. + + - 1 - clean label (given label is likely correct). + - 0 - dirty label (given label is likely incorrect). + + Parameters + ---------- + labels: + A list of ``N`` dictionaries such that ``labels[i]`` contains the given labels for the `i`-th image. + Refer to documentation for this argument in :py:func:`find_label_issues ` for further details. + + predictions: + A list of ``N`` ``np.ndarray`` such that ``predictions[i]`` corresponds to the model predictions for the `i`-th image. + Refer to documentation for this argument in :py:func:`find_label_issues ` for further details. + + aggregation_weights: + Optional dictionary to specify weights for aggregating quality scores for subtype of label issue into an overall label quality score for the image. + Its keys are: "overlooked", "swap", "badloc", and values should be nonnegative weights that sum to 1. + Increase one of these weights to prioritize images with bounding boxes that were either: + missing in the annotations (overlooked object), annotated with the wrong class label (class for the object should be swapped to another class), or annotated in a suboptimal location (badly located). + + swapped examples, bad location examples, and overlooked examples. + It is important to ensure that the weights are non-negative values and that their sum equals 1.0. + + verbose : bool, default = True + Set to ``False`` to suppress all print statements. + + Returns + --------- + label_quality_scores: + Array of shape ``(N, )`` of scores between 0 and 1, one per image in the object detection dataset. + Lower scores indicate images that are more likely mislabeled. + """ + method = "objectlab" + probability_threshold = 0.0 + + assert_valid_inputs( + labels=labels, + predictions=predictions, + method=method, + threshold=probability_threshold, + ) + aggregation_weights = _get_aggregation_weights(aggregation_weights) + + return _compute_label_quality_scores( + labels=labels, + predictions=predictions, + method=method, + threshold=probability_threshold, + aggregation_weights=aggregation_weights, + verbose=verbose, + ) + + +def issues_from_scores(label_quality_scores: np.ndarray, *, threshold: float = 0.1) -> np.ndarray: + """Convert label quality scores to a list of indices of images with issues sorted from most to least severe cut off at threshold. + + Returns the list of indices of images with issues sorted from most to least severe cut off at threshold. + + Parameters + ---------- + label_quality_scores: + Array of shape ``(N, )`` of scores between 0 and 1, one per image in the object detection dataset. + Lower scores indicate images are more likely to contain a label issue. + + threshold: + Label quality scores above the threshold are not considered to be label issues. The corresponding examples' indices are omitted from the returned array. + + Returns + --------- + issue_indices: + Array of issue indices sorted from most to least severe who's label quality scores fall below the threshold if one is provided. + """ + + if threshold > 1.0: + raise ValueError( + f""" + Threshold is a cutoff of label_quality_scores and therefore should be <= 1. + """ + ) + + issue_indices = np.argwhere(label_quality_scores <= threshold).flatten() + issue_vals = label_quality_scores[issue_indices] + sorted_idx = issue_vals.argsort() + return issue_indices[sorted_idx] + + +def _compute_label_quality_scores( + labels: List[Dict[str, Any]], + predictions: List[np.ndarray], + aggregation_weights: Optional[Dict[str, float]] = None, + *, + method: str = "objectlab", + threshold: Optional[float] = None, + verbose: bool = True, +) -> np.ndarray: + """Internal function to prune extra bounding boxes and compute label quality scores based on passed in method.""" + + pred_probs_prepruned = False + min_pred_prob = _get_min_pred_prob(predictions) + aggregation_weights = _get_aggregation_weights(aggregation_weights) + + if threshold is not None: + predictions = _prune_by_threshold( + predictions=predictions, threshold=threshold, verbose=verbose + ) + if np.abs(min_pred_prob - threshold) < 0.001 and threshold > 0: + pred_probs_prepruned = True # the provided threshold is the threshold used for pre_pruning the pred_probs during model prediction. + else: + threshold = min_pred_prob # assume model was not pre_pruned if no threshold was provided + + if method == "objectlab": + scores = _get_subtype_label_quality_scores( + labels, + predictions, + alpha=ALPHA, + low_probability_threshold=LOW_PROBABILITY_THRESHOLD, + high_probability_threshold=HIGH_PROBABILITY_THRESHOLD, + temperature=TEMPERATURE, + aggregation_weights=aggregation_weights, + ) + + return scores + + +def _get_min_pred_prob( + predictions: List[np.ndarray], +) -> float: + """Returns min pred_prob out of all predictions.""" + pred_probs = [1.0] # avoid calling np.min on empty array. + for prediction in predictions: + for class_prediction in prediction: + pred_probs.extend(list(class_prediction[:, -1])) + + min_pred_prob = np.min(pred_probs) + return min_pred_prob + + +def _prune_by_threshold( + predictions: List[np.ndarray], threshold: float, verbose: bool = True +) -> List[np.ndarray]: + """Removes predicted bounding boxes from predictions who's pred_prob is below the cuttoff threshold.""" + + predictions_copy = copy.deepcopy(predictions) + num_ann_to_zero = 0 + total_ann = 0 + for idx_predictions, prediction in enumerate(predictions_copy): + for idx_class, class_prediction in enumerate(prediction): + filtered_class_prediction = class_prediction[class_prediction[:, -1] >= threshold] + if len(class_prediction) > 0: + total_ann += 1 + if len(filtered_class_prediction) == 0: + num_ann_to_zero += 1 + + predictions_copy[idx_predictions][idx_class] = filtered_class_prediction + + p_ann_pruned = total_ann and num_ann_to_zero / total_ann or 0 # avoid division by zero + if p_ann_pruned > MAX_ALLOWED_BOX_PRUNE: + warnings.warn( + f"Pruning with threshold=={threshold} prunes {p_ann_pruned}% labels. Consider lowering the threshold.", + UserWarning, + ) + if verbose: + print( + f"Pruning {num_ann_to_zero} predictions out of {total_ann} using threshold=={threshold}. These predictions are no longer considered as potential candidates for identifying label issues as their similarity with the given labels is no longer considered." + ) + return predictions_copy + + +def _separate_label(label: Dict[str, Any]) -> Tuple[np.ndarray, np.ndarray]: + """Separates labels into bounding box and class label lists.""" + bboxes = label["bboxes"] + labels = label["labels"] + return bboxes, labels + + +# TODO: make object detection work for all predicted probabilities +def _separate_prediction_all_preds( + prediction: List[np.ndarray], +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + pred_bboxes, pred_labels, det_probs = prediction + return pred_bboxes, pred_labels, det_probs + + +def _separate_prediction_single_box( + prediction: np.ndarray, +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Separates predictions into class labels, bounding boxes and pred_prob lists""" + labels = [] + boxes = [] + for idx, prediction_class in enumerate(prediction): + labels.extend([idx] * len(prediction_class)) + boxes.extend(prediction_class.tolist()) + bboxes = [box[:4] for box in boxes] + pred_probs = [box[-1] for box in boxes] + return np.array(bboxes), np.array(labels), np.array(pred_probs) + + +def _get_prediction_type(prediction: np.ndarray) -> str: + if ( + len(prediction) == 3 + and prediction[0].shape[0] == prediction[2].shape[1] + and prediction[1].shape[0] == prediction[2].shape[0] + ): + return "all_pred" + else: + return "single_pred" + + +def _separate_prediction( + prediction, prediction_type="single_pred" +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Returns bbox, label and pred_prob values for prediction.""" + + if prediction_type == "all_pred": + boxes, labels, pred_probs = _separate_prediction_all_preds(prediction) + else: + boxes, labels, pred_probs = _separate_prediction_single_box(prediction) + return boxes, labels, pred_probs + + +def _mod_coordinates(x: List[float]) -> Dict[str, Any]: + """Takes is a list of xyxy coordinates and returns them in dictionary format.""" + + wd = {"x1": x[0], "y1": x[1], "x2": x[2], "y2": x[3]} + return wd + + +def _get_overlap(bb1: List[float], bb2: List[float]) -> float: + """Takes in two bounding boxes `bb1` and `bb2` and returns their IoU overlap.""" + + return _get_iou(_mod_coordinates(bb1), _mod_coordinates(bb2)) + + +def _get_overlap_matrix(bb1_list: np.ndarray, bb2_list: np.ndarray) -> np.ndarray: + """Takes in two lists of bounding boxes and returns an IoU matrix where IoU[i][j] is the overlap between + the i-th box in `bb1_list` and the j-th box in `bb2_list`.""" + wd = np.zeros(shape=(len(bb1_list), len(bb2_list))) + for i in range(len(bb1_list)): + for j in range(len(bb2_list)): + wd[i][j] = _get_overlap(bb1_list[i], bb2_list[j]) + return wd + + +def _get_iou(bb1: Dict[str, Any], bb2: Dict[str, Any]) -> float: + """ + Calculate the Intersection over Union (IoU) of two bounding boxes. + I've modified this to calculate overlap ratio in the line: + iou = np.clip(intersection_area / float(min(bb1_area,bb2_area)),0.0,1.0) + + Parameters + ---------- + bb1 : dict + Keys: {'x1', 'x2', 'y1', 'y2'} + The (x1, y1) position is at the top left corner, + the (x2, y2) position is at the bottom right corner + bb2 : dict + Keys: {'x1', 'x2', 'y1', 'y2'} + The (x, y) position is at the top left corner, + the (x2, y2) position is at the bottom right corner + Returns + ------- + float + in [0, 1] + """ + # determine the coordinates of the intersection rectangle + x_left = max(bb1["x1"], bb2["x1"]) + y_top = max(bb1["y1"], bb2["y1"]) + x_right = min(bb1["x2"], bb2["x2"]) + y_bottom = min(bb1["y2"], bb2["y2"]) + + if x_right < x_left or y_bottom < y_top: + return 0.0 + + # The intersection of two axis-aligned bounding boxes is always an + # axis-aligned bounding box + intersection_area = (x_right - x_left) * (y_bottom - y_top) + + # compute the area of both AABBs + bb1_area = (bb1["x2"] - bb1["x1"]) * (bb1["y2"] - bb1["y1"]) + bb2_area = (bb2["x2"] - bb2["x1"]) * (bb2["y2"] - bb2["y1"]) + + # compute the intersection over union by taking the intersection + # area and dividing it by the sum of prediction + ground-truth + # areas - the interesection area + iou = intersection_area / float(bb1_area + bb2_area - intersection_area) + # There are some hyper-parameters here like consider tile area/object area + return iou + + +def _euc_dis(box1: List[float], box2: List[float]) -> float: + """Calculates the Euclidean distance between `box1` and `box2`.""" + x1, y1 = (box1[0] + box1[2]) / 2, (box1[1] + box1[3]) / 2 + x2, y2 = (box2[0] + box2[2]) / 2, (box2[1] + box2[3]) / 2 + p1 = np.array([x1, y1]) + p2 = np.array([x2, y2]) + val2 = np.exp(-np.linalg.norm(p1 - p2) * EUC_FACTOR) + return val2 + + +def _get_dist_matrix(bb1_list: np.ndarray, bb2_list: np.ndarray) -> np.ndarray: + """Returns a distance matrix of distances from all of boxes in bb1_list to all of boxes in bb2_list.""" + wd = np.zeros(shape=(len(bb1_list), len(bb2_list))) + for i in range(len(bb1_list)): + for j in range(len(bb2_list)): + wd[i][j] = _euc_dis(bb1_list[i], bb2_list[j]) + return wd + + +def _get_min_possible_similarity( + alpha: float, + predictions, + labels: List[Dict[str, Any]], +) -> float: + """Gets the min possible similarity score between two bounding boxes out of all images.""" + min_possible_similarity = 1.0 + for prediction, label in zip(predictions, labels): + lab_bboxes, lab_labels = _separate_label(label) + pred_bboxes, pred_labels, _ = _separate_prediction(prediction) + iou_matrix = _get_overlap_matrix(lab_bboxes, pred_bboxes) + dist_matrix = 1 - _get_dist_matrix(lab_bboxes, pred_bboxes) + similarity_matrix = iou_matrix * alpha + (1 - alpha) * (1 - dist_matrix) + non_zero_similarity_matrix = similarity_matrix[np.nonzero(similarity_matrix)] + min_image_similarity = ( + 1.0 if 0 in non_zero_similarity_matrix.shape else np.min(non_zero_similarity_matrix) + ) + min_possible_similarity = np.min([min_possible_similarity, min_image_similarity]) + return min_possible_similarity + + +def _get_valid_inputs_for_compute_scores_per_image( + *, + alpha: float, + label: Optional[Dict[str, Any]] = None, + prediction: Optional[np.ndarray] = None, + pred_labels: Optional[np.ndarray] = None, + pred_label_probs: Optional[np.ndarray] = None, + pred_bboxes: Optional[np.ndarray] = None, + lab_labels: Optional[np.ndarray] = None, + lab_bboxes=None, + similarity_matrix=None, + min_possible_similarity: Optional[float] = None, +) -> AuxiliaryTypesDict: + """Returns valid inputs for compute scores by either passing through values or calculating the inputs internally.""" + if lab_labels is None or lab_bboxes is None: + if label is None: + raise ValueError( + f"Pass in either one of label or label labels into auxiliary inputs. Both can not be None." + ) + lab_bboxes, lab_labels = _separate_label(label) + + if pred_labels is None or pred_label_probs is None or pred_bboxes is None: + if prediction is None: + raise ValueError( + f"Pass in either one of prediction or prediction labels and prediction probabilities into auxiliary inputs. Both can not be None." + ) + pred_bboxes, pred_labels, pred_label_probs = _separate_prediction(prediction) + + if similarity_matrix is None: + iou_matrix = _get_overlap_matrix(lab_bboxes, pred_bboxes) + dist_matrix = 1 - _get_dist_matrix(lab_bboxes, pred_bboxes) + similarity_matrix = iou_matrix * alpha + (1 - alpha) * (1 - dist_matrix) + + if min_possible_similarity is None: + min_possible_similarity = ( + 1.0 + if 0 in similarity_matrix.shape + else np.min(similarity_matrix[np.nonzero(similarity_matrix)]) + ) + + auxiliary_input_dict: AuxiliaryTypesDict = { + "pred_labels": pred_labels, + "pred_label_probs": pred_label_probs, + "pred_bboxes": pred_bboxes, + "lab_labels": lab_labels, + "lab_bboxes": lab_bboxes, + "similarity_matrix": similarity_matrix, + "min_possible_similarity": min_possible_similarity, + } + + return auxiliary_input_dict + + +def _get_valid_inputs_for_compute_scores( + alpha: float, + labels: Optional[List[Dict[str, Any]]] = None, + predictions: Optional[List[np.ndarray]] = None, +) -> List[AuxiliaryTypesDict]: + """Takes in alpha, labels and predictions and returns auxiliary input dictionary containing divided parts of labels and prediction per image.""" + if predictions is None or labels is None: + raise ValueError( + f"Predictions and labels can not be None. Both are needed to get valid inputs." + ) + min_possible_similarity = _get_min_possible_similarity(alpha, predictions, labels) + + auxiliary_inputs = [] + + for prediction, label in zip(predictions, labels): + auxiliary_input_dict = _get_valid_inputs_for_compute_scores_per_image( + alpha=alpha, + label=label, + prediction=prediction, + min_possible_similarity=min_possible_similarity, + ) + auxiliary_inputs.append(auxiliary_input_dict) + + return auxiliary_inputs + + +def _get_valid_score(scores_arr: np.ndarray, temperature: float) -> float: + """Given scores array, returns valid score (softmin) or 1. Checks validity of score.""" + scores_arr = scores_arr[~np.isnan(scores_arr)] + if len(scores_arr) > 0: + valid_score = softmin1d(scores_arr, temperature=temperature) + else: + valid_score = 1.0 + return valid_score + + +def _get_valid_subtype_score_params( + alpha: Optional[float] = None, + low_probability_threshold: Optional[float] = None, + high_probability_threshold: Optional[float] = None, + temperature: Optional[float] = None, +): + """This function returns valid params for subtype score. If param is None, then default constant is returned""" + if alpha is None: + alpha = ALPHA + if low_probability_threshold is None: + low_probability_threshold = LOW_PROBABILITY_THRESHOLD + if high_probability_threshold is None: + high_probability_threshold = HIGH_PROBABILITY_THRESHOLD + if temperature is None: + temperature = TEMPERATURE + return alpha, low_probability_threshold, high_probability_threshold, temperature + + +def _get_aggregation_weights( + aggregation_weights: Optional[Dict[str, Any]] = None +) -> Dict[str, Any]: + """This function validates aggregation weights, returning the default weights if none are provided.""" + if aggregation_weights is None: + aggregation_weights = { + "overlooked": CUSTOM_SCORE_WEIGHT_OVERLOOKED, + "swap": CUSTOM_SCORE_WEIGHT_SWAP, + "badloc": CUSTOM_SCORE_WEIGHT_BADLOC, + } + else: + assert_valid_aggregation_weights(aggregation_weights) + return aggregation_weights + + +def _compute_overlooked_box_scores_for_image( + alpha: float, + high_probability_threshold: float, + label: Optional[Dict[str, Any]] = None, + prediction: Optional[np.ndarray] = None, + pred_labels: Optional[np.ndarray] = None, + pred_label_probs: Optional[np.ndarray] = None, + pred_bboxes: Optional[np.ndarray] = None, + lab_labels: Optional[np.ndarray] = None, + lab_bboxes: Optional[np.ndarray] = None, + similarity_matrix: Optional[np.ndarray] = None, + min_possible_similarity: Optional[float] = None, +) -> np.ndarray: + """This method returns one score per predicted box (above threshold) in an image. Score from 0 to 1 ranking how overlooked the box is.""" + + auxiliary_input_dict = _get_valid_inputs_for_compute_scores_per_image( + alpha=alpha, + label=label, + prediction=prediction, + pred_labels=pred_labels, + pred_label_probs=pred_label_probs, + pred_bboxes=pred_bboxes, + lab_labels=lab_labels, + lab_bboxes=lab_bboxes, + similarity_matrix=similarity_matrix, + min_possible_similarity=min_possible_similarity, + ) + + pred_labels = auxiliary_input_dict["pred_labels"] + pred_label_probs = auxiliary_input_dict["pred_label_probs"] + lab_labels = auxiliary_input_dict["lab_labels"] + similarity_matrix = auxiliary_input_dict["similarity_matrix"] + min_possible_similarity = auxiliary_input_dict["min_possible_similarity"] + + scores_overlooked = np.empty( + shape=[ + len(pred_labels), + ] + ) # same length as num of predicted boxes + + for iid, k in enumerate(pred_labels): + if pred_label_probs[iid] < high_probability_threshold: + scores_overlooked[iid] = np.nan + continue + + k_similarity = similarity_matrix[lab_labels == k, iid] + if len(k_similarity) == 0: # if there is no annotated box + score = min_possible_similarity * (1 - pred_label_probs[iid]) + else: + closest_annotated_box = np.argmax(k_similarity) + score = k_similarity[closest_annotated_box] + scores_overlooked[iid] = score + + return scores_overlooked + + +def compute_overlooked_box_scores( + labels: Optional[List[Dict[str, Any]]] = None, + predictions: Optional[List[np.ndarray]] = None, + *, + alpha: Optional[float] = None, + high_probability_threshold: Optional[float] = None, + auxiliary_inputs: Optional[List[AuxiliaryTypesDict]] = None, +) -> List[np.ndarray]: + """ + Returns an array of overlooked box scores for each image. + Score per high-confidence predicted bounding box is between 0 and 1, with lower values indicating boxes we are more confident were overlooked in the given label. + + Each image has ``L`` annotated bounding boxes and ``M`` predicted bounding boxes. + A score is calculated for each predicted box in each of the ``N`` images in dataset. + + Note: ``M`` and ``L`` can be a different values for each image, as the number of annotated and predicted boxes varies. + + Parameters + ---------- + labels: + A list of ``N`` dictionaries such that ``labels[i]`` contains the given labels for the `i`-th image. + Refer to documentation for this argument in :py:func:`find_label_issues ` for further details. + + predictions: + A list of ``N`` ``np.ndarray`` such that ``predictions[i]`` corresponds to the model predictions for the `i`-th image. + Refer to documentation for this argument in :py:func:`find_label_issues ` for further details. + + alpha: + Optional weighting between IoU and Euclidean distance when calculating similarity between predicted and annotated boxes. High alpha means weighting IoU more heavily over Euclidean distance. If no alpha is provided, a good default is used. + + high_probability_threshold: + Optional probability threshold that determines which predicted boxes are considered high-confidence when computing overlooked scores. If not provided, a good default is used. + + auxiliary_inputs: + Optional list of ``N`` dictionaries containing keys for sub-parts of label and prediction per image. Useful to minimize computation when computing multiple box scores for a single set of images. For the `i`-th image, `auxiliary_inputs[i]` should contain following keys: + + * pred_labels: np.ndarray + Array of predicted classes for `i`-th image of shape ``(M,)``. + * pred_label_probs: np.ndarray + Array of predicted class probabilities for `i`-th image of shape ``(M,)``. + * pred_bboxes: np.ndarray + Array of predicted bounding boxes for `i`-th image of shape ``(M, 4)``. + * lab_labels: np.ndarray + Array of given label classed for `i`-th image of shape ``(L,)``. + * lab_bboxes: np.ndarray + Array of given label bounding boxes for `i`-th image of shape ``(L, 4)``. + * similarity_matrix: np.ndarray + Similarity matrix between labels and predictions `i`-th image. + * min_possible_similarity: float + Minimum possible similarity value greater than 0 between labels and predictions for the entire dataset. + Returns + --------- + scores_overlooked: + A list of ``N`` numpy arrays where scores_overlooked[i] is an array of size ``M`` of overlooked scores per predicted box for the `i`-th image. + """ + ( + alpha, + low_probability_threshold, + high_probability_threshold, + temperature, + ) = _get_valid_subtype_score_params(alpha, None, high_probability_threshold, None) + + if auxiliary_inputs is None: + auxiliary_inputs = _get_valid_inputs_for_compute_scores(alpha, labels, predictions) + + scores_overlooked = [] + for auxiliary_input_dict in auxiliary_inputs: + scores_overlooked_per_box = _compute_overlooked_box_scores_for_image( + alpha=alpha, + high_probability_threshold=high_probability_threshold, + **auxiliary_input_dict, + ) + scores_overlooked.append(scores_overlooked_per_box) + return scores_overlooked + + +def _compute_badloc_box_scores_for_image( + alpha: float, + low_probability_threshold: float, + label: Optional[Dict[str, Any]] = None, + prediction: Optional[np.ndarray] = None, + pred_labels: Optional[np.ndarray] = None, + pred_label_probs: Optional[np.ndarray] = None, + pred_bboxes: Optional[np.ndarray] = None, + lab_labels: Optional[np.ndarray] = None, + lab_bboxes: Optional[np.ndarray] = None, + similarity_matrix: Optional[np.ndarray] = None, + min_possible_similarity: Optional[float] = None, +) -> np.ndarray: + """This method returns one score per labeled box in an image. Score from 0 to 1 ranking how badly located the box is.""" + + auxiliary_input_dict = _get_valid_inputs_for_compute_scores_per_image( + alpha=alpha, + label=label, + prediction=prediction, + pred_labels=pred_labels, + pred_label_probs=pred_label_probs, + pred_bboxes=pred_bboxes, + lab_labels=lab_labels, + lab_bboxes=lab_bboxes, + similarity_matrix=similarity_matrix, + min_possible_similarity=min_possible_similarity, + ) + + pred_labels = auxiliary_input_dict["pred_labels"] + pred_label_probs = auxiliary_input_dict["pred_label_probs"] + lab_labels = auxiliary_input_dict["lab_labels"] + similarity_matrix = auxiliary_input_dict["similarity_matrix"] + + scores_badloc = np.empty( + shape=[ + len(lab_labels), + ] + ) # same length as number of labeled boxes + for iid, k in enumerate(lab_labels): # for every annotated box + k_similarity = similarity_matrix[iid, pred_labels == k] + k_pred = pred_label_probs[pred_labels == k] + + if len(k_pred) == 0: # there are no predicted boxes of class k + scores_badloc[iid] = 1.0 + continue + + idx_at_least_low_probability_threshold = k_pred > low_probability_threshold + k_similarity = k_similarity[idx_at_least_low_probability_threshold] + k_pred = k_pred[idx_at_least_low_probability_threshold] + + if len(k_pred) == 0: + scores_badloc[iid] = 1.0 + else: + scores_badloc[iid] = np.max(k_similarity) + return scores_badloc + + +def compute_badloc_box_scores( + labels: Optional[List[Dict[str, Any]]] = None, + predictions: Optional[List[np.ndarray]] = None, + *, + alpha: Optional[float] = None, + low_probability_threshold: Optional[float] = None, + auxiliary_inputs: Optional[List[AuxiliaryTypesDict]] = None, +) -> List[np.ndarray]: + """ + Returns a numeric score for each annotated bounding box in each image, estimating the likelihood that the edges of this box are not badly located. + Score per high-confidence predicted bounding box is between 0 and 1, with lower values indicating boxes we are more confident were overlooked in the given label. + + Each image has ``L`` annotated bounding boxes and ``M`` predicted bounding boxes. + A score is calculated for each predicted box in each of the ``N`` images in dataset. + + Note: ``M`` and ``L`` can be a different values for each image, as the number of annotated and predicted boxes varies. + + Parameters + ---------- + labels: + A list of ``N`` dictionaries such that ``labels[i]`` contains the given labels for the `i`-th image. + Refer to documentation for this argument in :py:func:`find_label_issues ` for further details. + + predictions: + A list of ``N`` ``np.ndarray`` such that ``predictions[i]`` corresponds to the model predictions for the `i`-th image. + Refer to documentation for this argument in :py:func:`find_label_issues ` for further details. + + alpha: + Optional weighting between IoU and Euclidean distance when calculating similarity between predicted and annotated boxes. High alpha means weighting IoU more heavily over Euclidean distance. If no alpha is provided, a good default is used. + + low_probability_threshold: + Optional minimum probability threshold that determines which predicted boxes are considered when computing badly located scores. If not provided, a good default is used. + + auxiliary_inputs: + Optional list of ``N`` dictionaries containing keys for sub-parts of label and prediction per image. Useful to minimize computation when computing multiple box scores for a single set of images. For the `i`-th image, `auxiliary_inputs[i]` should contain following keys: + + * pred_labels: np.ndarray + Array of predicted classes for `i`-th image of shape ``(M,)``. + * pred_label_probs: np.ndarray + Array of predicted class probabilities for `i`-th image of shape ``(M,)``. + * pred_bboxes: np.ndarray + Array of predicted bounding boxes for `i`-th image of shape ``(M, 4)``. + * lab_labels: np.ndarray + Array of given label classed for `i`-th image of shape ``(L,)``. + * lab_bboxes: np.ndarray + Array of given label bounding boxes for `i`-th image of shape ``(L, 4)``. + * similarity_matrix: np.ndarray + Similarity matrix between labels and predictions `i`-th image. + * min_possible_similarity: float + Minimum possible similarity value greater than 0 between labels and predictions for the entire dataset. + Returns + --------- + scores_badloc: + A list of ``N`` numpy arrays where scores_badloc[i] is an array of size ``L`` badly located scores per annotated box for the `i`-th image. + """ + ( + alpha, + low_probability_threshold, + high_probability_threshold, + temperature, + ) = _get_valid_subtype_score_params(alpha, low_probability_threshold, None, None) + + if auxiliary_inputs is None: + auxiliary_inputs = _get_valid_inputs_for_compute_scores(alpha, labels, predictions) + + scores_badloc = [] + for auxiliary_input_dict in auxiliary_inputs: + scores_badloc_per_box = _compute_badloc_box_scores_for_image( + alpha=alpha, low_probability_threshold=low_probability_threshold, **auxiliary_input_dict + ) + scores_badloc.append(scores_badloc_per_box) + return scores_badloc + + +def _compute_swap_box_scores_for_image( + alpha: float, + high_probability_threshold: float, + label: Optional[Dict[str, Any]] = None, + prediction: Optional[np.ndarray] = None, + pred_labels: Optional[np.ndarray] = None, + pred_label_probs: Optional[np.ndarray] = None, + pred_bboxes: Optional[np.ndarray] = None, + lab_labels: Optional[np.ndarray] = None, + lab_bboxes: Optional[np.ndarray] = None, + similarity_matrix: Optional[np.ndarray] = None, + min_possible_similarity: Optional[float] = None, +) -> np.ndarray: + """This method returns one score per labeled box in an image. Score from 0 to 1 ranking how likeley swapped the box is.""" + + auxiliary_input_dict = _get_valid_inputs_for_compute_scores_per_image( + alpha=alpha, + label=label, + prediction=prediction, + pred_labels=pred_labels, + pred_label_probs=pred_label_probs, + pred_bboxes=pred_bboxes, + lab_labels=lab_labels, + lab_bboxes=lab_bboxes, + similarity_matrix=similarity_matrix, + min_possible_similarity=min_possible_similarity, + ) + + pred_labels = auxiliary_input_dict["pred_labels"] + pred_label_probs = auxiliary_input_dict["pred_label_probs"] + lab_labels = auxiliary_input_dict["lab_labels"] + similarity_matrix = auxiliary_input_dict["similarity_matrix"] + min_possible_similarity = auxiliary_input_dict["min_possible_similarity"] + + scores_swap = np.empty( + shape=[ + len(lab_labels), + ] + ) # same length as number of labeled boxes + for iid, k in enumerate(lab_labels): + not_k_idx = pred_labels != k + + if len(not_k_idx) == 0: + scores_swap[iid] = 1.0 + continue + + not_k_similarity = similarity_matrix[iid, not_k_idx] + not_k_pred = pred_label_probs[not_k_idx] + + idx_at_least_high_probability_threshold = not_k_pred > high_probability_threshold + if len(idx_at_least_high_probability_threshold) == 0: + scores_swap[iid] = 1.0 + continue + + not_k_similarity = not_k_similarity[idx_at_least_high_probability_threshold] + if len(not_k_similarity) == 0: # if there is no annotated box + scores_swap[iid] = 1.0 + else: + closest_predicted_box = np.argmax(not_k_similarity) + score = np.max([min_possible_similarity, 1 - not_k_similarity[closest_predicted_box]]) + scores_swap[iid] = score + return scores_swap + + +def compute_swap_box_scores( + labels: Optional[List[Dict[str, Any]]] = None, + predictions: Optional[List[np.ndarray]] = None, + *, + alpha: Optional[float] = None, + high_probability_threshold: Optional[float] = None, + auxiliary_inputs: Optional[List[AuxiliaryTypesDict]] = None, +) -> List[np.ndarray]: + """ + Returns a numeric score for each annotated bounding box in each image, estimating the likelihood that the class label for this box was not accidentally swapped with another class. + Score per high-confidence predicted bounding box is between 0 and 1, with lower values indicating boxes we are more confident were overlooked in the given label. + + Each image has ``L`` annotated bounding boxes and ``M`` predicted bounding boxes. + A score is calculated for each predicted box in each of the ``N`` images in dataset. + + Note: ``M`` and ``L`` can be a different values for each image, as the number of annotated and predicted boxes varies. + + Parameters + ---------- + labels: + A list of ``N`` dictionaries such that ``labels[i]`` contains the given labels for the `i`-th image. + Refer to documentation for this argument in :py:func:`find_label_issues ` for further details. + + predictions: + A list of ``N`` ``np.ndarray`` such that ``predictions[i]`` corresponds to the model predictions for the `i`-th image. + Refer to documentation for this argument in :py:func:`find_label_issues ` for further details. + + alpha: + Optional weighting between IoU and Euclidean distance when calculating similarity between predicted and annotated boxes. High alpha means weighting IoU more heavily over Euclidean distance. If no alpha is provided, a good default is used. + + high_probability_threshold: + Optional probability threshold that determines which predicted boxes are considered high-confidence when computing overlooked scores. If not provided, a good default is used. + + auxiliary_inputs: + Optional list of ``N`` dictionaries containing keys for sub-parts of label and prediction per image. Useful to minimize computation when computing multiple box scores for a single set of images. For the `i`-th image, `auxiliary_inputs[i]` should contain following keys: + + * pred_labels: np.ndarray + Array of predicted classes for `i`-th image of shape ``(M,)``. + * pred_label_probs: np.ndarray + Array of predicted class probabilities for `i`-th image of shape ``(M,)``. + * pred_bboxes: np.ndarray + Array of predicted bounding boxes for `i`-th image of shape ``(M, 4)``. + * lab_labels: np.ndarray + Array of given label classed for `i`-th image of shape ``(L,)``. + * lab_bboxes: np.ndarray + Array of given label bounding boxes for `i`-th image of shape ``(L, 4)``. + * similarity_matrix: np.ndarray + Similarity matrix between labels and predictions `i`-th image. + * min_possible_similarity: float + Minimum possible similarity value greater than 0 between labels and predictions for the entire dataset. + Returns + --------- + scores_swap: + A list of ``N`` numpy arrays where scores_swap[i] is an array of size ``L`` swap scores per annotated box for the `i`-th image. + """ + ( + alpha, + low_probability_threshold, + high_probability_threshold, + temperature, + ) = _get_valid_subtype_score_params(alpha, None, high_probability_threshold, None) + + if auxiliary_inputs is None: + auxiliary_inputs = _get_valid_inputs_for_compute_scores(alpha, labels, predictions) + + scores_swap = [] + for auxiliary_inputs in auxiliary_inputs: + scores_swap_per_box = _compute_swap_box_scores_for_image( + alpha=alpha, high_probability_threshold=high_probability_threshold, **auxiliary_inputs + ) + scores_swap.append(scores_swap_per_box) + return scores_swap + + +def pool_box_scores_per_image( + box_scores: List[np.ndarray], *, temperature: Optional[float] = None +) -> np.ndarray: + """ + Aggregates multiple per-box scores within an image to return a single quality score for the image rather than for individual boxes within it. + Score per image is between 0 and 1, with lower values indicating we are more confident image contains an error. + + Parameters + ---------- + box_scores: + A list of ``N`` numpy arrays where box_scores[i] is an array of badly located scores per box for the `i`-th image. + + temperature: + Optional temperature of the softmin function where a lower value suggests softmin acts closer to min. If not provided, a good default is used. + + Returns + --------- + image_scores: + An array of size ``N`` where ``image_scores[i]`` represents the score for the `i`-th image. + """ + + ( + alpha, + low_probability_threshold, + high_probability_threshold, + temperature, + ) = _get_valid_subtype_score_params(None, None, None, temperature) + + image_scores = np.empty( + shape=[ + len(box_scores), + ] + ) + for idx, box_score in enumerate(box_scores): + image_score = _get_valid_score(box_score, temperature=temperature) + image_scores[idx] = image_score + return image_scores + + +def _get_subtype_label_quality_scores( + labels: List[Dict[str, Any]], + predictions: List[np.ndarray], + *, + alpha: Optional[float] = None, + low_probability_threshold: Optional[float] = None, + high_probability_threshold: Optional[float] = None, + temperature: Optional[float] = None, + aggregation_weights: Optional[Dict[str, float]] = None, +) -> np.ndarray: + """ + Returns a label quality score for each of the ``N`` images in the dataset. + Score is between 0 and 1. + + 1 - clean label (given label is likely correct). + 0 - dirty label (given label is likely incorrect). + + Parameters + ---------- + labels: + A list of ``N`` dictionaries such that ``labels[i]`` contains the given labels for the `i`-th image. + Refer to documentation for this argument in :py:func:`find_label_issues ` for further details. + + predictions: + A list of ``N`` ``np.ndarray`` such that ``predictions[i]`` corresponds to the model predictions for the `i`-th image. + Refer to documentation for this argument in :py:func:`find_label_issues ` for further details. + + alpha: + Optional weighting between IoU and Euclidean distance when calculating similarity between predicted and annotated boxes. High alpha means weighting IoU more heavily over Euclidean distance. If no alpha is provided, a good default is used. + + low_probability_threshold: + Optional minimum probability threshold that determines which predicted boxes are considered when computing badly located scores. If not provided, a good default is used. + + high_probability_threshold: + Optional probability threshold that determines which predicted boxes are considered high-confidence when computing overlooked and swapped scores. If not provided, a good default is used. + + temperature: + Optional temperature of the softmin function where a lower score suggests softmin acts closer to min. If not provided, a good default is used. + + Returns + --------- + label_quality_scores: + As returned by :py:func:`get_label_quality_scores `. See function for more details. + """ + ( + alpha, + low_probability_threshold, + high_probability_threshold, + temperature, + ) = _get_valid_subtype_score_params( + alpha, low_probability_threshold, high_probability_threshold, temperature + ) + auxiliary_inputs = _get_valid_inputs_for_compute_scores(alpha, labels, predictions) + aggregation_weights = _get_aggregation_weights(aggregation_weights) + + overlooked_scores_per_box = compute_overlooked_box_scores( + alpha=alpha, + high_probability_threshold=high_probability_threshold, + auxiliary_inputs=auxiliary_inputs, + ) + overlooked_score_per_image = pool_box_scores_per_image( + overlooked_scores_per_box, temperature=temperature + ) + + badloc_scores_per_box = compute_badloc_box_scores( + alpha=alpha, + low_probability_threshold=low_probability_threshold, + auxiliary_inputs=auxiliary_inputs, + ) + badloc_score_per_image = pool_box_scores_per_image( + badloc_scores_per_box, temperature=temperature + ) + + swap_scores_per_box = compute_swap_box_scores( + alpha=alpha, + high_probability_threshold=high_probability_threshold, + auxiliary_inputs=auxiliary_inputs, + ) + swap_score_per_image = pool_box_scores_per_image(swap_scores_per_box, temperature=temperature) + + scores = ( + aggregation_weights["overlooked"] * np.log(TINY_VALUE + overlooked_score_per_image) + + aggregation_weights["badloc"] * np.log(TINY_VALUE + badloc_score_per_image) + + aggregation_weights["swap"] * np.log(TINY_VALUE + swap_score_per_image) + ) + + scores = np.exp(scores) + + return scores diff --git a/cleanlab/object_detection/summary.py b/cleanlab/object_detection/summary.py new file mode 100644 index 0000000000..1e83ee89f7 --- /dev/null +++ b/cleanlab/object_detection/summary.py @@ -0,0 +1,239 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . + +"""Methods to display examples and their label issues in an object detection dataset.""" +from typing import Optional, Any, Dict, Tuple, Union, TYPE_CHECKING, TypeVar + +import numpy as np + +from cleanlab.internal.constants import MAX_CLASS_TO_SHOW +from cleanlab.object_detection.rank import ( + _separate_prediction, + _separate_label, + _get_prediction_type, +) + +from cleanlab.internal.object_detection_utils import bbox_xyxy_to_xywh + +if TYPE_CHECKING: + from PIL.Image import Image as Image # pragma: no cover +else: + Image = TypeVar("Image") + + +def visualize( + image: Union[str, Image], + *, + label: Optional[Dict[str, Any]] = None, + prediction: Optional[np.ndarray] = None, + prediction_threshold: Optional[float] = None, + overlay: bool = True, + class_names: Optional[Dict[Any, Any]] = None, + figsize: Optional[Tuple[int, int]] = None, + save_path: Optional[str] = None, +) -> None: + """Display the annotated bounding boxes (given labels) and predicted bounding boxes (model predictions) for a particular image. + Given labels are shown in red, model predictions in blue. + + + Parameters + ---------- + image: + Image object loaded into memory or full path to the image file. If path is provided, image is loaded into memory. + + label: + The given label for a single image in the format ``{'bboxes': np.ndarray((L,4)), 'labels': np.ndarray((L,))}`` where + ``L`` is the number of bounding boxes for the `i`-th image and ``bboxes[j]`` is in the format ``[x1,y1,x2,y2]`` with given label ``labels[j]``. + + Note: Here, ``[x1,y1]`` corresponds to the coordinates of the bottom-left corner of the bounding box, while ``[x2,y2]`` corresponds to the coordinates of the top-right corner of the bounding box. The last column, pred_prob, represents the predicted probability that the bounding box contains an object of the class k. + + prediction: + A prediction for a single image in the format ``np.ndarray((K,))`` and ``prediction[k]`` is of shape ``np.ndarray(N,5)`` + where ``M`` is the number of predicted bounding boxes for class ``k`` and the five columns correspond to ``[x,y,x,y,pred_prob]`` where + ``[x,y,x,y]`` are the bounding box coordinates predicted by the model and ``pred_prob`` is the model's confidence in ``predictions[i]``. + + prediction_threshold: + All model-predicted bounding boxes with confidence (`pred_prob`) + below this threshold are omitted from the visualization. + + overlay: bool + If True, display a single image with given labels and predictions overlaid. + If False, display two images (side by side) with the left image showing the model predictions and the right image showing the given label. + + class_names: + Optional dictionary mapping one-hot-encoded class labels back to their original class names in the format ``{"integer-label": "original-class-name"}``. + + save_path: + Path to save figure at. If a path is provided, the figure is saved. To save in a specific image format, add desired file extension to the end of `save_path`. Allowed file extensions are: 'png', 'pdf', 'ps', 'eps', and 'svg'. + + figsize: + Optional figure size for plotting the image. + Corresponds to ``matplotlib.figure.figsize``. + """ + try: + import matplotlib.pyplot as plt + except ImportError as e: + raise ImportError( + "This functionality requires matplotlib. Install it via: `pip install matplotlib`" + ) + + # Create figure and axes + if isinstance(image, str): + image = plt.imread(image) + + if prediction is not None: + prediction_type = _get_prediction_type(prediction) + pbbox, plabels, pred_probs = _separate_prediction( + prediction, prediction_type=prediction_type + ) + + if prediction_threshold is not None: + keep_idx = np.where(pred_probs > prediction_threshold) + pbbox = pbbox[keep_idx] + plabels = plabels[keep_idx] + + if label is not None: + abbox, alabels = _separate_label(label) + + if overlay: + figsize = (8, 5) if figsize is None else figsize + fig, ax = plt.subplots(frameon=False, figsize=figsize) + plt.axis("off") + ax.imshow(image) + if label is not None: + fig, ax = _draw_boxes( + fig, ax, abbox, alabels, edgecolor="r", linestyle="-", linewidth=1 + ) + if prediction is not None: + _, _ = _draw_boxes(fig, ax, pbbox, plabels, edgecolor="b", linestyle="-.", linewidth=1) + else: + figsize = (14, 10) if figsize is None else figsize + fig, axes = plt.subplots(nrows=1, ncols=2, frameon=False, figsize=figsize) + axes[0].axis("off") + axes[0].imshow(image) + axes[1].axis("off") + axes[1].imshow(image) + + if label is not None: + fig, ax = _draw_boxes( + fig, axes[0], abbox, alabels, edgecolor="r", linestyle="-", linewidth=1 + ) + if prediction is not None: + _, _ = _draw_boxes( + fig, axes[1], pbbox, plabels, edgecolor="b", linestyle="-.", linewidth=1 + ) + bbox_extra_artists = None + if label or prediction is not None: + legend, plt = _plot_legend(class_names, label, prediction) + bbox_extra_artists = (legend,) + + if save_path: + allowed_image_formats = set(["png", "pdf", "ps", "eps", "svg"]) + image_format: Optional[str] = None + if save_path.split(".")[-1] in allowed_image_formats and "." in save_path: + image_format = save_path.split(".")[-1] + plt.savefig( + save_path, + format=image_format, + bbox_extra_artists=bbox_extra_artists, + bbox_inches="tight", + transparent=True, + pad_inches=0.5, + ) + plt.show() + + +def _plot_legend(class_names, label, prediction): + colors = ["black"] + colors.extend(["red"] if label is not None else []) + colors.extend(["blue"] if prediction is not None else []) + + markers = [None] + markers.extend(["s"] if label is not None else []) + markers.extend(["s"] if prediction is not None else []) + + labels = [r"$\bf{Legend}$"] + labels.extend(["given label"] if label is not None else []) + labels.extend(["predicted label"] if prediction is not None else []) + + if class_names: + colors += ["black"] + ["black"] * min(len(class_names), MAX_CLASS_TO_SHOW) + markers += [None] + [f"${class_key}$" for class_key in class_names.keys()] + labels += [r"$\bf{classes}$"] + list(class_names.values()) + + try: + import matplotlib.pyplot as plt + except ImportError as e: + raise ImportError( + "This functionality requires matplotlib. Install it via: `pip install matplotlib`" + ) + + f = lambda m, c: plt.plot([], [], marker=m, color=c, ls="none")[0] + handles = [f(marker, color) for marker, color in zip(markers, colors)] + legend = plt.legend( + handles, labels, bbox_to_anchor=(1.04, 0.05), loc="lower left", borderaxespad=0 + ) + + return legend, plt + + +def _draw_labels(ax, rect, label, edgecolor): + """Helper function to draw labels on an axis.""" + + rx, ry = rect.get_xy() + c_xleft = rx + 10 + c_xright = rx + rect.get_width() - 10 + c_ytop = ry + 12 + + if edgecolor == "r": + cx, cy = c_xleft, c_ytop + else: # edgecolor == b + cx, cy = c_xright, c_ytop + + l = ax.annotate( + label, (cx, cy), fontsize=8, fontweight="bold", color="white", ha="center", va="center" + ) + l.set_bbox(dict(facecolor=edgecolor, alpha=0.35, edgecolor=edgecolor, pad=2)) + return ax + + +def _draw_boxes(fig, ax, bboxes, labels, edgecolor="g", linestyle="-", linewidth=3): + """Helper function to draw bboxes and labels on an axis.""" + bboxes = [bbox_xyxy_to_xywh(box) for box in bboxes] + + try: + from matplotlib.patches import Rectangle + except Exception as e: + raise ImportError( + "This functionality requires matplotlib. Install it via: `pip install matplotlib`" + ) + + for (x, y, w, h), label in zip(bboxes, labels): + rect = Rectangle( + (x, y), + w, + h, + linewidth=linewidth, + linestyle=linestyle, + edgecolor=edgecolor, + facecolor="none", + ) + ax.add_patch(rect) + + if labels is not None: + ax = _draw_labels(ax, rect, label, edgecolor) + + return fig, ax diff --git a/cleanlab/outlier.py b/cleanlab/outlier.py index b2190ada65..8fb50e8252 100644 --- a/cleanlab/outlier.py +++ b/cleanlab/outlier.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -25,11 +25,12 @@ from cleanlab.count import get_confident_thresholds from sklearn.neighbors import NearestNeighbors from sklearn.exceptions import NotFittedError -from typing import Optional, Union, Tuple, Dict +from typing import Optional, Union, Tuple, Dict, cast from cleanlab.internal.label_quality_utils import ( _subtract_confident_thresholds, get_normalized_entropy, ) +from cleanlab.internal.outlier import transform_distances_to_scores from cleanlab.internal.validation import assert_valid_inputs, labels_to_array from cleanlab.typing import LabelLike @@ -37,10 +38,11 @@ class OutOfDistribution: """ Provides scores to detect Out Of Distribution (OOD) examples that are outliers in a dataset. + Each example's OOD score lies in [0,1] with smaller values indicating examples that are less typical under the data distribution. OOD scores may be estimated from either: numeric feature embeddings or predicted probabilities from a trained classifier. - To get indices of examples that are the most severe outliers, call :py:func:`find_top_issues ` function on the returned OOD scores. + To get indices of examples that are the most severe outliers, call `~cleanlab.rank.find_top_issues` function on the returned OOD scores. Parameters ---------- @@ -55,7 +57,8 @@ class OutOfDistribution: You can also pass in a subclass of ``sklearn.neighbors.NearestNeighbors`` which allows you to use faster approximate neighbor libraries as long as you wrap them behind the same sklearn API. If you specify ``knn`` here, there is no need to later call ``fit()`` before calling ``score()``. - If ``knn = None``, then by default: ``knn = sklearn.neighbors.NearestNeighbors(n_neighbors=k, metric="cosine").fit(features)`` + If ``knn = None``, then by default: ``knn = sklearn.neighbors.NearestNeighbors(n_neighbors=k, metric=dist_metric).fit(features)`` + where ``dist_metric == "cosine"`` if ``dim(features) > 3`` or ``dist_metric == "euclidean"`` otherwise. See: https://scikit-learn.org/stable/modules/neighbors.html * k : int, default=None Optional number of neighbors to use when calculating outlier score (average distance to neighbors). @@ -92,7 +95,7 @@ class OutOfDistribution: OUTLIER_PARAMS = {"k", "t", "knn"} OOD_PARAMS = {"confident_thresholds", "adjust_pred_probs", "method"} - DEFAULT_PARAM_DICT: Dict[Union[str, int, None], Union[str, int, None, np.ndarray]] = { + DEFAULT_PARAM_DICT: Dict[str, Union[str, int, None, np.ndarray]] = { "k": None, # ood features param "t": 1, # ood features param "knn": None, # ood features param @@ -101,10 +104,11 @@ class OutOfDistribution: "confident_thresholds": None, # ood pred_probs param } - def __init__(self, params: dict = {}): + def __init__(self, params: Optional[dict] = None) -> None: self._assert_valid_params(params, self.DEFAULT_PARAM_DICT) - self.params = self.DEFAULT_PARAM_DICT - self.params = {**self.params, **params} + self.params = self.DEFAULT_PARAM_DICT.copy() + if params is not None: + self.params.update(params) def fit_score( self, @@ -116,27 +120,27 @@ def fit_score( ) -> np.ndarray: """ Fits this estimator to a given dataset and returns out-of-distribution scores for the same dataset. + Scores lie in [0,1] with smaller values indicating examples that are less typical under the dataset distribution (values near 0 indicate outliers). Exactly one of `features` or `pred_probs` needs to be passed in to calculate scores. If `features` are passed in a ``NearestNeighbors`` object is fit. If `pred_probs` and 'labels' are passed in a - `confident_thresholds` ``np.ndarray`` is fit. For details see :py:func:`fit - `. + `confident_thresholds` ``np.ndarray`` is fit. For details see `~cleanlab.outlier.OutOfDistribution.fit`. Parameters ---------- features : np.ndarray, optional Feature array of shape ``(N, M)``, where N is the number of examples and M is the number of features used to represent each example. - For details, `features` in the same format expected by the :py:func:`fit ` function. + For details, `features` in the same format expected by the `~cleanlab.outlier.OutOfDistribution.fit` function. pred_probs : np.ndarray, optional An array of shape ``(N, K)`` of predicted class probabilities output by a trained classifier. - For details, `pred_probs` in the same format expected by the :py:func:`fit ` function. + For details, `pred_probs` in the same format expected by the `~cleanlab.outlier.OutOfDistribution.fit` function. labels : array_like, optional A discrete array of given class labels for the data of shape ``(N,)``. - For details, `labels` in the same format expected by the :py:func:`fit ` function. + For details, `labels` in the same format expected by the `~cleanlab.outlier.OutOfDistribution.fit` function. verbose : bool, default = True Set to ``False`` to suppress all print statements. @@ -146,7 +150,7 @@ def fit_score( scores : np.ndarray If `features` are passed in, `ood_features_scores` are returned. If `pred_probs` are passed in, `ood_predictions_scores` are returned. - For details see return of :py:func:`score ` function. + For details see return of `~cleanlab.outlier.OutOfDistribution.scores` function. """ scores = self._shared_fit( @@ -171,18 +175,19 @@ def fit( ): """ Fits this estimator to a given dataset. + One of `features` or `pred_probs` must be specified. If `features` are passed in, a ``NearestNeighbors`` object is fit. If `pred_probs` and 'labels' are passed in, a `confident_thresholds` ``np.ndarray`` is fit. - For details see :py:class:`OutOfDistribution ` documentation. + For details see `~cleanlab.outlier.OutOfDistribution` documentation. Parameters ---------- features : np.ndarray, optional Feature array of shape ``(N, M)``, where N is the number of examples and M is the number of features used to represent each example. - All features should be **numeric**. For less structured data (eg. images, text, categorical values, ...), you should provide - vector embeddings to represent each example (eg. extracted from some pretrained neural network). + All features should be **numeric**. For less structured data (e.g. images, text, categorical values, ...), you should provide + vector embeddings to represent each example (e.g. extracted from some pretrained neural network). pred_probs : np.ndarray, optional An array of shape ``(N, K)`` of model-predicted probabilities, @@ -214,7 +219,8 @@ def score( self, *, features: Optional[np.ndarray] = None, pred_probs: Optional[np.ndarray] = None ) -> np.ndarray: """ - Uses fitted estimator and passed in `features` or `pred_probs` to calculate out-of-distribution scores for a dataset. + Use fitted estimator and passed in `features` or `pred_probs` to calculate out-of-distribution scores for a dataset. + Score for each example corresponds to the likelihood this example stems from the same distribution as the dataset previously specified in ``fit()`` (i.e. is not an outlier). If `features` are passed, returns OOD score for each example based on its feature values. @@ -225,11 +231,11 @@ def score( ---------- features : np.ndarray, optional Feature array of shape ``(N, M)``, where N is the number of examples and M is the number of features used to represent each example. - For details, see `features` in :py:func:`fit ` function. + For details, see `features` in `~cleanlab.outlier.OutOfDistribution.fit` function. pred_probs : np.ndarray, optional An array of shape ``(N, K)`` of predicted class probabilities output by a trained classifier. - For details, see `pred_probs` in :py:func:`fit ` function. + For details, see `pred_probs` in `~cleanlab.outlier.OutOfDistribution.fit` function. Returns ------- @@ -249,37 +255,27 @@ def score( if features is not None: if self.params["knn"] is None: raise ValueError( - f"OOD estimator needs to be fit on features first. Call `fit()` or `fit_scores()` before this function." - ) - else: - scores, _ = _get_ood_features_scores( - features, **self._get_params(self.OUTLIER_PARAMS) + "OOD estimator needs to be fit on features first. Call `fit()` or `fit_scores()` before this function." ) + scores, _ = _get_ood_features_scores(features, **self._get_params(self.OUTLIER_PARAMS)) if pred_probs is not None: if self.params["confident_thresholds"] is None and self.params["adjust_pred_probs"]: raise ValueError( - f"OOD estimator needs to be fit on pred_probs first since params['adjust_pred_probs']=True. Call `fit()` or `fit_scores()` before this function." - ) - else: - scores, _ = _get_ood_predictions_scores( - pred_probs, **self._get_params(self.OOD_PARAMS) + "OOD estimator needs to be fit on pred_probs first since params['adjust_pred_probs']=True. Call `fit()` or `fit_scores()` before this function." ) + scores, _ = _get_ood_predictions_scores(pred_probs, **self._get_params(self.OOD_PARAMS)) return scores def _get_params(self, param_keys) -> dict: - """ - Helper method to get function specific dictionary of parameters (i.e. only those in param_keys). - """ + """Get function specific dictionary of parameters (i.e. only those in param_keys).""" return {k: v for k, v in self.params.items() if k in param_keys} @staticmethod def _assert_valid_params(params, param_keys): - """ - Helper method to check passed in params valid and get list of parameters in param that are not in param_keys. - """ - if len(params) > 0: + """Validate passed in params and get list of parameters in param that are not in param_keys.""" + if params is not None: wrong_params = list(set(params.keys()).difference(set(param_keys))) if len(wrong_params) > 0: raise ValueError( @@ -288,23 +284,21 @@ def _assert_valid_params(params, param_keys): @staticmethod def _assert_valid_inputs(features, pred_probs): - """ - Helper method to check features and pred_prob inputs are valid. Throws error if not. - """ + """Check whether features and pred_prob inputs are valid, throw error if not.""" if features is None and pred_probs is None: raise ValueError( - f"Not enough information to compute scores. Pass in either features or pred_probs." + "Not enough information to compute scores. Pass in either features or pred_probs." ) if features is not None and pred_probs is not None: raise ValueError( - f"Cannot fit to OOD Estimator to both features and pred_probs. Pass in either one or the other." + "Cannot fit to OOD Estimator to both features and pred_probs. Pass in either one or the other." ) if features is not None and len(features.shape) != 2: raise ValueError( - f"Feature array needs to be of shape (N, M), where N is the number of examples and M is the " - f"number of features used to represent each example. " + "Feature array needs to be of shape (N, M), where N is the number of examples and M is the " + "number of features used to represent each example. " ) def _shared_fit( @@ -317,8 +311,9 @@ def _shared_fit( ) -> Optional[np.ndarray]: """ Shared fit functionality between ``fit()`` and ``fit_score()``. - For details, refer to :py:func:`fit ` - or :py:func:`fit_score `. + + For details, refer to `~cleanlab.outlier.OutOfDistribution.fit` + or `~cleanlab.outlier.OutOfDistribution.fit_score`. """ self._assert_valid_inputs(features, pred_probs) scores = None # If none scores are returned, fit was skipped @@ -327,7 +322,7 @@ def _shared_fit( if self.params["knn"] is not None: # No fitting twice if knn object already fit warnings.warn( - f"A KNN estimator has previously already been fit, call score() to apply it to data, or create a new OutOfDistribution object to fit a different estimator.", + "A KNN estimator has previously already been fit, call score() to apply it to data, or create a new OutOfDistribution object to fit a different estimator.", UserWarning, ) else: @@ -343,7 +338,7 @@ def _shared_fit( if self.params["confident_thresholds"] is not None: # No fitting twice if confident_thresholds object already fit warnings.warn( - f"Confident thresholds have previously already been fit, call score() to apply them to data, or create a new OutOfDistribution object to fit a different estimator.", + "Confident thresholds have previously already been fit, call score() to apply them to data, or create a new OutOfDistribution object to fit a different estimator.", UserWarning, ) else: @@ -357,8 +352,8 @@ def _shared_fit( ) if confident_thresholds is None: warnings.warn( - f"No estimates need to be be fit under the provided params, so you could directly call " - f"score() as an alternative.", + "No estimates need to be be fit under the provided params, so you could directly call " + "score() as an alternative.", UserWarning, ) else: @@ -372,25 +367,28 @@ def _get_ood_features_scores( k: Optional[int] = None, t: int = 1, ) -> Tuple[np.ndarray, Optional[NearestNeighbors]]: - """Returns an outlier score for each example based on its feature values which is computed inversely proportional - to the average distance between this example and its K nearest neighbors (in feature space). + """ + Return outlier score based on feature values using `k` nearest neighbors. + + The outlier score for each example is computed inversely proportional to + the average distance between this example and its K nearest neighbors (in feature space). Parameters ---------- features : np.ndarray Feature array of shape ``(N, M)``, where N is the number of examples and M is the number of features used to represent each example. - For details, `features` in the same format expected by the :py:func:`fit ` function. + For details, `features` in the same format expected by the `~cleanlab.outlier.OutOfDistribution.fit` function. knn : sklearn.neighbors.NearestNeighbors, default = None - For details, see key `knn` in the params dict arg of :py:class:`OutOfDistribution `. + For details, see key `knn` in the params dict arg of `~cleanlab.outlier.OutOfDistribution`. k : int, default=None Optional number of neighbors to use when calculating outlier score (average distance to neighbors). - For details, see key `k` in the params dict arg of :py:class:`OutOfDistribution `. + For details, see key `k` in the params dict arg of `~cleanlab.outlier.OutOfDistribution`. t : int, default=1 Controls transformation of distances between examples into similarity scores that lie in [0,1]. - For details, see key `t` in the params dict arg of :py:class:`OutOfDistribution `. + For details, see key `t` in the params dict arg of `~cleanlab.outlier.OutOfDistribution`. Returns ------- @@ -403,7 +401,7 @@ def _get_ood_features_scores( # Make sure both knn and features are not None if features is None: raise ValueError( - f"Both knn and features arguments cannot be None at the same time. Not enough information to compute outlier scores." + "Both knn and features arguments cannot be None at the same time. Not enough information to compute outlier scores." ) if k is None: k = DEFAULT_K # use default when knn and k are both None @@ -411,8 +409,15 @@ def _get_ood_features_scores( raise ValueError( f"Number of nearest neighbors k={k} cannot exceed the number of examples N={len(features)} passed into the estimator (knn)." ) - knn = NearestNeighbors(n_neighbors=k, metric="cosine").fit(features) + + if features.shape[1] > 3: # use euclidean distance for lower dimensional spaces + metric = "cosine" + else: + metric = "euclidean" + + knn = NearestNeighbors(n_neighbors=k, metric=metric).fit(features) features = None # features should be None in knn.kneighbors(features) to avoid counting duplicate data points + elif k is None: k = knn.n_neighbors @@ -428,7 +433,7 @@ def _get_ood_features_scores( # Fit knn estimator on the features if a non-fitted estimator is passed in try: knn.kneighbors(features) - except NotFittedError as e: + except NotFittedError: knn.fit(features) # Get distances to k-nearest neighbors Note that the knn object contains the specification of distance metric @@ -436,11 +441,7 @@ def _get_ood_features_scores( # neighbor of each point is the point itself, at a distance of zero. distances, _ = knn.kneighbors(features) - # Calculate average distance to k-nearest neighbors - avg_knn_distances = distances[:, :k].mean(axis=1) - - # Map ood_features_scores to range 0-1 with 0 = most concerning - ood_features_scores: np.ndarray = np.exp(-1 * avg_knn_distances * t) + ood_features_scores = transform_distances_to_scores(distances, cast(int, k), t) return (ood_features_scores, knn) @@ -452,27 +453,27 @@ def _get_ood_predictions_scores( adjust_pred_probs: bool = True, method: str = "entropy", ) -> Tuple[np.ndarray, Optional[np.ndarray]]: - """Returns an OOD (out of distribution) score for each example based on it pred_prob values. + """Return an OOD (out of distribution) score for each example based on it pred_prob values. Parameters ---------- pred_probs : np.ndarray An array of shape ``(N, K)`` of model-predicted probabilities, - `pred_probs` in the same format expected by the :py:func:`fit ` function. + `pred_probs` in the same format expected by the `~cleanlab.outlier.OutOfDistribution.fit` function. confident_thresholds : np.ndarray, default = None - For details, see key `confident_thresholds` in the params dict arg of :py:class:`OutOfDistribution `. + For details, see key `confident_thresholds` in the params dict arg of `~cleanlab.outlier.OutOfDistribution`. labels : array_like, optional - `labels` in the same format expected by the :py:func:`fit ` function. + `labels` in the same format expected by the `~cleanlab.outlier.OutOfDistribution.fit` function. adjust_pred_probs : bool, True Account for class imbalance in the label-quality scoring. - For details, see key `adjust_pred_probs` in the params dict arg of :py:class:`OutOfDistribution `. + For details, see key `adjust_pred_probs` in the params dict arg of `~cleanlab.outlier.OutOfDistribution`. method : {"entropy", "least_confidence"}, default="entropy" OOD scoring method. - For details see key `method` in the params dict arg of :py:class:`OutOfDistribution `. + For details see key `method` in the params dict arg of `~cleanlab.outlier.OutOfDistribution`. Returns @@ -480,17 +481,16 @@ def _get_ood_predictions_scores( ood_predictions_scores : Tuple[np.ndarray, Optional[np.ndarray]] Returns a tuple. First element is array of `ood_predictions_scores` and second is an np.ndarray of `confident_thresholds` or None is 'confident_thresholds' is not calculated. """ - - valid_methods = [ + valid_methods = ( "entropy", "least_confidence", - ] + ) if (confident_thresholds is not None or labels is not None) and not adjust_pred_probs: warnings.warn( - f"OOD scores are not adjusted with confident thresholds. If scores need to be adjusted set " - f"params['adjusted_pred_probs'] = True. Otherwise passing in confident_thresholds and/or labels does not change " - f"score calculation.", + "OOD scores are not adjusted with confident thresholds. If scores need to be adjusted set " + "params['adjusted_pred_probs'] = True. Otherwise passing in confident_thresholds and/or labels does not change " + "score calculation.", UserWarning, ) @@ -498,15 +498,12 @@ def _get_ood_predictions_scores( if confident_thresholds is None: if labels is None: raise ValueError( - f"Cannot calculate adjust_pred_probs without labels. Either pass in labels parameter or set " - f"params['adjusted_pred_probs'] = False. " - ) - else: - labels = labels_to_array(labels) - assert_valid_inputs(X=None, y=labels, pred_probs=pred_probs, multi_label=False) - confident_thresholds = get_confident_thresholds( - labels, pred_probs, multi_label=False + "Cannot calculate adjust_pred_probs without labels. Either pass in labels parameter or set " + "params['adjusted_pred_probs'] = False. " ) + labels = labels_to_array(labels) + assert_valid_inputs(X=None, y=labels, pred_probs=pred_probs, multi_label=False) + confident_thresholds = get_confident_thresholds(labels, pred_probs, multi_label=False) pred_probs = _subtract_confident_thresholds( None, pred_probs, multi_label=False, confident_thresholds=confident_thresholds diff --git a/cleanlab/rank.py b/cleanlab/rank.py index ef2e03b5a3..9e51da9fd6 100644 --- a/cleanlab/rank.py +++ b/cleanlab/rank.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -25,6 +25,7 @@ Note: multi-label classification is not supported by most methods in this module, each example must be labeled as belonging to a single class, e.g. format: ``labels = np.ndarray([1,0,2,1,1,0...])``. +For multi-label classification, instead see :py:func:`multilabel_classification.get_label_quality_scores `. Note: Label quality scores are most accurate when they are computed based on out-of-sample `pred_probs` from your model. To obtain out-of-sample predicted probabilities for every datapoint in your dataset, you can use :ref:`cross-validation `. This is encouraged to get better results. @@ -36,6 +37,10 @@ import warnings from cleanlab.internal.validation import assert_valid_inputs +from cleanlab.internal.constants import ( + CLIPPING_LOWER_BOUND, +) # lower-bound clipping threshold to prevents 0 in logs and division + from cleanlab.internal.label_quality_utils import ( _subtract_confident_thresholds, get_normalized_entropy, @@ -119,15 +124,28 @@ class 0, 1, ..., K-1. assert_valid_inputs( X=None, y=labels, pred_probs=pred_probs, multi_label=False, allow_one_class=True ) + return _compute_label_quality_scores( + labels=labels, pred_probs=pred_probs, method=method, adjust_pred_probs=adjust_pred_probs + ) + - # Available scoring functions to choose from +def _compute_label_quality_scores( + labels: np.ndarray, + pred_probs: np.ndarray, + *, + method: str = "self_confidence", + adjust_pred_probs: bool = False, + confident_thresholds: Optional[np.ndarray] = None, +) -> np.ndarray: + """Internal implementation of get_label_quality_scores that assumes inputs + have already been checked and are valid. This speeds things up. + Can also take in pre-computed confident_thresholds to further accelerate things. + """ scoring_funcs = { "self_confidence": get_self_confidence_for_each_label, "normalized_margin": get_normalized_margin_for_each_label, "confidence_weighted_entropy": get_confidence_weighted_entropy_for_each_label, } - - # Select scoring function try: scoring_func = scoring_funcs[method] except KeyError: @@ -137,22 +155,15 @@ class 0, 1, ..., K-1. Please choose a valid rank_by: self_confidence, normalized_margin, confidence_weighted_entropy """ ) - - # Adjust predicted probabilities if adjust_pred_probs: - - # Check if adjust_pred_probs is supported for the chosen method if method == "confidence_weighted_entropy": raise ValueError(f"adjust_pred_probs is not currently supported for {method}.") + pred_probs = _subtract_confident_thresholds( + labels=labels, pred_probs=pred_probs, confident_thresholds=confident_thresholds + ) - pred_probs = _subtract_confident_thresholds(labels, pred_probs) - - # Pass keyword arguments for scoring function - input = {"labels": labels, "pred_probs": pred_probs} - - # Calculate scores - label_quality_scores = scoring_func(**input) - + scoring_inputs = {"labels": labels, "pred_probs": pred_probs} + label_quality_scores = scoring_func(**scoring_inputs) return label_quality_scores @@ -229,8 +240,6 @@ def get_label_quality_ensemble_scores( get_label_quality_scores """ - MIN_ALLOWED = 1e-6 # lower-bound clipping threshold to prevents 0 in logs and division - # Check pred_probs_list for errors assert isinstance( pred_probs_list, list @@ -259,20 +268,18 @@ def get_label_quality_ensemble_scores( # This weighting scheme performs search of t in log_loss_search_T_values for "best" log loss if weight_ensemble_members_by == "log_loss_search": - # Initialize variables for log loss search pred_probs_avg_log_loss_weighted = None neg_log_loss_weights = None best_eval_log_loss = float("inf") for t in log_loss_search_T_values: - neg_log_loss_list = [] # pred_probs for each model for pred_probs in pred_probs_list: pred_probs_clipped = np.clip( - pred_probs, a_min=MIN_ALLOWED, a_max=None + pred_probs, a_min=CLIPPING_LOWER_BOUND, a_max=None ) # lower-bound clipping threshold to prevents 0 in logs when calculating log loss pred_probs_clipped /= pred_probs_clipped.sum(axis=1)[:, np.newaxis] # renormalize @@ -299,7 +306,6 @@ def get_label_quality_ensemble_scores( scores_list = [] accuracy_list = [] for pred_probs in pred_probs_list: - # Calculate scores and accuracy scores = get_label_quality_scores( labels=labels, @@ -349,7 +355,6 @@ def get_label_quality_ensemble_scores( label_quality_scores = (scores_ensemble * weights).sum(axis=1) elif weight_ensemble_members_by == "custom": - # Check custom_weights for errors assert ( custom_weights is not None @@ -499,9 +504,9 @@ def get_self_confidence_for_each_label( Lower scores indicate more likely mislabeled examples. """ - # np.mean is used so that this works for multi-labels (list of lists) - label_quality_scores = np.array([np.mean(pred_probs[i, l]) for i, l in enumerate(labels)]) - return label_quality_scores + # To make this work for multi-label (but it will slow down runtime), replace: + # pred_probs[i, l] -> np.mean(pred_probs[i, l]) + return np.array([pred_probs[i, l] for i, l in enumerate(labels)]) def get_normalized_margin_for_each_label( @@ -571,15 +576,14 @@ def get_confidence_weighted_entropy_for_each_label( Lower scores indicate more likely mislabeled examples. """ - MIN_ALLOWED = 1e-6 # lower-bound clipping threshold to prevents 0 in logs and division self_confidence = get_self_confidence_for_each_label(labels, pred_probs) - self_confidence = np.clip(self_confidence, a_min=MIN_ALLOWED, a_max=None) + self_confidence = np.clip(self_confidence, a_min=CLIPPING_LOWER_BOUND, a_max=None) # Divide entropy by self confidence label_quality_scores = get_normalized_entropy(pred_probs) / self_confidence # Rescale - clipped_scores = np.clip(label_quality_scores, a_min=MIN_ALLOWED, a_max=None) + clipped_scores = np.clip(label_quality_scores, a_min=CLIPPING_LOWER_BOUND, a_max=None) label_quality_scores = np.log(label_quality_scores + 1) / clipped_scores return label_quality_scores diff --git a/cleanlab/regression/__init__.py b/cleanlab/regression/__init__.py new file mode 100644 index 0000000000..9928af71eb --- /dev/null +++ b/cleanlab/regression/__init__.py @@ -0,0 +1,2 @@ +from . import rank +from . import learn diff --git a/cleanlab/regression/learn.py b/cleanlab/regression/learn.py new file mode 100644 index 0000000000..d026741f9a --- /dev/null +++ b/cleanlab/regression/learn.py @@ -0,0 +1,880 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . + +""" +cleanlab can be used for learning with noisy data for any dataset and regression model. + +For regression tasks, the :py:class:`regression.learn.CleanLearning ` +class wraps any instance of an sklearn model to allow you to train more robust regression models, +or use the model to identify corrupted values in the dataset. +The wrapped model must adhere to the `sklearn estimator API +`_, +meaning it must define three functions: + +* ``model.fit(X, y, sample_weight=None)`` +* ``model.predict(X)`` +* ``model.score(X, y, sample_weight=None)`` + +where ``X`` contains the data (i.e. features, covariates, independant variables) and ``y`` contains the target +value (i.e. label, response/dependant variable). The first index of ``X`` and of ``y`` should correspond to the different +examples in the dataset, such that ``len(X) = len(y) = N`` (sample-size). + +Your model should be correctly clonable via +`sklearn.base.clone `_: +cleanlab internally creates multiple instances of the model, and if you e.g. manually wrap a +PyTorch model, ensure that every call to the estimator's ``__init__()`` creates an independent +instance of the model (for sklearn compatibility, the weights of neural network models should typically +be initialized inside of ``clf.fit()``). + +Example +------- +>>> from cleanlab.regression.learn import CleanLearning +>>> from sklearn.linear_model import LinearRegression +>>> cl = CleanLearning(clf=LinearRegression()) # Pass in any model. +>>> cl.fit(X, y_with_noise) +>>> # Estimate the predictions as if you had trained without label issues. +>>> predictions = cl.predict(y) + +If your model is not sklearn-compatible by default, it might be the case that standard packages can adapt +the model. For example, you can adapt PyTorch models using `skorch `_ +and adapt Keras models using `SciKeras `_. + +If an adapter doesn't already exist, you can manually wrap your +model to be sklearn-compatible. This is made easy by inheriting from +`sklearn.base.BaseEstimator +`_: + +.. code:: python + + from sklearn.base import BaseEstimator + + class YourModel(BaseEstimator): + def __init__(self, ): + pass + def fit(self, X, y): + pass + def predict(self, X): + pass + def score(self, X, y): + pass + +""" + +from typing import Optional, Union, Tuple +import inspect +import warnings + +import math +import numpy as np +import pandas as pd + +import sklearn.base +from sklearn.base import BaseEstimator +from sklearn.model_selection import KFold +from sklearn.linear_model import LinearRegression +from sklearn.metrics import r2_score + +from cleanlab.typing import LabelLike +from cleanlab.internal.constants import TINY_VALUE +from cleanlab.internal.util import train_val_split, subset_X_y +from cleanlab.internal.regression_utils import assert_valid_regression_inputs +from cleanlab.internal.validation import labels_to_array + + +class CleanLearning(BaseEstimator): + """ + CleanLearning = Machine Learning with cleaned data (even when training on messy, error-ridden data). + + Automated and robust learning with noisy labels using any dataset and any regression model. + For regression tasks, this class trains a ``model`` with error-prone, noisy labels + as if the model had been instead trained on a dataset with perfect labels. + It achieves this by estimating which labels are noisy (you might solely use CleanLearning for this estimation) + and then removing examples estimated to have noisy labels, such that a more robust copy of the same model can be + trained on the remaining clean data. + + Parameters + ---------- + model : + Any regression model implementing the `sklearn estimator API `_, + defining the following functions: + + - ``model.fit(X, y)`` + - ``model.predict(X)`` + - ``model.score(X, y)`` + + Default model used is `sklearn.linear_model.LinearRegression + `_. + + cv_n_folds : + This class needs holdout predictions for every data example and if not provided, + uses cross-validation to compute them. This argument sets the number of cross-validation + folds used to compute out-of-sample predictions for each example in ``X``. Default is 5. + Larger values may produce better results, but requires longer to run. + + n_boot : + Number of bootstrap resampling rounds used to estimate the model's epistemic uncertainty. + Default is 5. Larger values are expected to produce better results but require longer runtimes. + + include_aleatoric_uncertainty : + Specifies if the aleatoric uncertainty should be estimated during label error detection. + ``True`` by default, which is expected to produce better results but require longer runtimes. + + verbose : + Controls how much output is printed. Set to ``False`` to suppress print statements. Default `False`. + + seed : + Set the default state of the random number generator used to split + the data. By default, uses ``np.random`` current random state. + """ + + def __init__( + self, + model: Optional[BaseEstimator] = None, + *, + cv_n_folds: int = 5, + n_boot: int = 5, + include_aleatoric_uncertainty: bool = True, + verbose: bool = False, + seed: Optional[bool] = None, + ): + if model is None: + # Use linear regression if no model is provided. + model = LinearRegression() + + # Make sure the given regression model has the appropriate methods defined. + if not hasattr(model, "fit"): + raise ValueError("The model must define a .fit() method.") + if not hasattr(model, "predict"): + raise ValueError("The model must define a .predict() method.") + + if seed is not None: + np.random.seed(seed=seed) + + if n_boot < 0: + raise ValueError("n_boot cannot be a negative value") + if cv_n_folds < 2: + raise ValueError("cv_n_folds must be at least 2") + + self.model: BaseEstimator = model + self.seed: Optional[int] = seed + self.cv_n_folds: int = cv_n_folds + self.n_boot: int = n_boot + self.include_aleatoric_uncertainty: bool = include_aleatoric_uncertainty + self.verbose: bool = verbose + self.label_issues_df: Optional[pd.DataFrame] = None + self.label_issues_mask: Optional[np.ndarray] = None + self.k: Optional[float] = None # frac flagged as issue + + def fit( + self, + X: Union[np.ndarray, pd.DataFrame], + y: LabelLike, + *, + label_issues: Optional[Union[pd.DataFrame, np.ndarray]] = None, + sample_weight: Optional[np.ndarray] = None, + find_label_issues_kwargs: Optional[dict] = None, + model_kwargs: Optional[dict] = None, + model_final_kwargs: Optional[dict] = None, + ) -> BaseEstimator: + """ + Train regression ``model`` with error-prone, noisy labels as if the model had been instead trained + on a dataset with the correct labels. ``fit`` achieves this by first training ``model`` via + cross-validation on the noisy data, using the resulting predicted probabilities to identify label issues, + pruning the data with label issues, and finally training ``model`` on the remaining clean data. + + Parameters + ---------- + X : + Data features (i.e. covariates, independent variables), typically an array of shape ``(N, ...)``, + where N is the number of examples (sample-size). + Your ``model`` must be able to ``fit()`` and ``predict()`` data of this format. + + y : + An array of shape ``(N,)`` of noisy labels (i.e. target/response/dependant variable), where some values may be erroneous. + + label_issues : + Optional already-identified label issues in the dataset (if previously estimated). + Specify this to avoid re-estimating the label issues if already done. + If ``pd.DataFrame``, must be formatted as the one returned by: + :py:meth:`self.find_label_issues ` or + :py:meth:`self.get_label_issues `. The DataFrame must + have a column named ``is_label_issue``. + + If ``np.ndarray``, the input must be a boolean mask of length ``N`` where examples that have label issues + have the value ``True``, and the rest of the examples have the value ``False``. + + sample_weight : + Optional array of weights with shape ``(N,)`` that are assigned to individual samples. Specifies how to weight the examples in + the loss function while training. + + find_label_issues_kwargs: + Optional keyword arguments to pass into :py:meth:`self.find_label_issues `. + + model_kwargs : + Optional keyword arguments to pass into model's ``fit()`` method. + + model_final_kwargs : + Optional extra keyword arguments to pass into the final model's ``fit()`` on the cleaned data, + but not the ``fit()`` in each fold of cross-validation on the noisy data. + The final ``fit()`` will also receive the arguments in `clf_kwargs`, but these may be overwritten + by values in `clf_final_kwargs`. This can be useful for training differently in the final ``fit()`` + than during cross-validation. + + Returns + ------- + self : CleanLearning + Fitted estimator that has all the same methods as any sklearn estimator. + + After calling ``self.fit()``, this estimator also stores extra attributes such as: + + - ``self.label_issues_df``: a ``pd.DataFrame`` containing label quality scores, boolean flags + indicating which examples have label issues, and predicted label values for each example. + Accessible via :py:meth:`self.get_label_issues `, + of similar format as the one returned by :py:meth:`self.find_label_issues `. + See documentation of :py:meth:`self.find_label_issues ` + for column descriptions. + - ``self.label_issues_mask``: a ``np.ndarray`` boolean mask indicating if a particular + example has been identified to have issues. + """ + assert_valid_regression_inputs(X, y) + + if find_label_issues_kwargs is None: + find_label_issues_kwargs = {} + if model_kwargs is None: + model_kwargs = {} + if model_final_kwargs is None: + model_final_kwargs = {} + model_final_kwargs = {**model_kwargs, **model_final_kwargs} + + if "sample_weight" in model_kwargs or "sample_weight" in model_final_kwargs: + raise ValueError( + "sample_weight should be provided directly in fit() rather than in model_kwargs or model_final_kwargs" + ) + + if sample_weight is not None: + if "sample_weight" not in inspect.getfullargspec(self.model.fit).args: + raise ValueError( + "sample_weight must be a supported fit() argument for your model in order to be specified here" + ) + if len(sample_weight) != len(X): + raise ValueError("sample_weight must be a 1D array that has the same length as y.") + + if label_issues is None: + if self.label_issues_df is not None and self.verbose: + print( + "If you already ran self.find_label_issues() and don't want to recompute, you " + "should pass the label_issues in as a parameter to this function next time." + ) + + label_issues = self.find_label_issues( + X, + y, + model_kwargs=model_kwargs, + **find_label_issues_kwargs, + ) + else: + if self.verbose: + print("Using provided label_issues instead of finding label issues.") + if self.label_issues_df is not None: + print( + "These will overwrite self.label_issues_df and will be returned by " + "`self.get_label_issues()`. " + ) + + self.label_issues_df = self._process_label_issues_arg(label_issues, y) + self.label_issues_mask = self.label_issues_df["is_label_issue"].to_numpy() + + X_mask = np.invert(self.label_issues_mask) + X_cleaned, y_cleaned = subset_X_y(X, y, X_mask) + if self.verbose: + print(f"Pruning {np.sum(self.label_issues_mask)} examples with label issues ...") + print(f"Remaining clean data has {len(y_cleaned)} examples.") + + if sample_weight is not None: + model_final_kwargs["sample_weight"] = sample_weight[X_mask] + if self.verbose: + print("Fitting final model on the clean data with custom sample_weight ...") + else: + if self.verbose: + print("Fitting final model on the clean data ...") + + self.model.fit(X_cleaned, y_cleaned, **model_final_kwargs) + + if self.verbose: + print( + "Label issues stored in label_issues_df DataFrame accessible via: self.get_label_issues(). " + "Call self.save_space() to delete this potentially large DataFrame attribute." + ) + return self + + def predict(self, X: np.ndarray, *args, **kwargs) -> np.ndarray: + """ + Predict class labels using your wrapped model. + Works just like ``model.predict()``. + + Parameters + ---------- + X : np.ndarray or DatasetLike + Test data in the same format expected by your wrapped regression model. + + Returns + ------- + predictions : np.ndarray + Predictions for the test examples. + """ + return self.model.predict(X, *args, **kwargs) + + def score( + self, + X: Union[np.ndarray, pd.DataFrame], + y: LabelLike, + sample_weight: Optional[np.ndarray] = None, + ) -> float: + """Evaluates your wrapped regression model's score on a test set `X` with target values `y`. + Uses your model's default scoring function, or r-squared score if your model as no ``"score"`` attribute. + + Parameters + ---------- + X : + Test data in the same format expected by your wrapped model. + + y : + Test labels in the same format as labels previously used in ``fit()``. + + sample_weight : + Optional array of shape ``(N,)`` or ``(N, 1)`` used to weight each test example when computing the score. + + Returns + ------- + score : float + Number quantifying the performance of this regression model on the test data. + """ + if hasattr(self.model, "score"): + if "sample_weight" in inspect.getfullargspec(self.model.score).args: + return self.model.score(X, y, sample_weight=sample_weight) + else: + return self.model.score(X, y) + else: + return r2_score( + y, + self.model.predict(X), + sample_weight=sample_weight, + ) + + def find_label_issues( + self, + X: Union[np.ndarray, pd.DataFrame], + y: LabelLike, + *, + uncertainty: Optional[Union[np.ndarray, float]] = None, + coarse_search_range: list = [0.01, 0.05, 0.1, 0.15, 0.2], + fine_search_size: int = 3, + save_space: bool = False, + model_kwargs: Optional[dict] = None, + ) -> pd.DataFrame: + """ + Identifies potential label issues (corrupted `y`-values) in the dataset, and estimates how noisy each label is. + + Note: this method estimates the label issues from scratch. To access previously-estimated label issues from + this :py:class:`CleanLearning ` instance, use the + :py:meth:`self.get_label_issues ` method. + + This is the method called to find label issues inside + :py:meth:`CleanLearning.fit() ` + and they share mostly the same parameters. + + Parameters + ---------- + X : + Data features (i.e. covariates, independent variables), typically an array of shape ``(N, ...)``, + where N is the number of examples (sample-size). + Your ``model``, must be able to ``fit()`` and ``predict()`` data of this format. + + y : + An array of shape ``(N,)`` of noisy labels (i.e. target/response/dependant variable), where some values may be erroneous. + + uncertainty : + Optional estimated uncertainty for each example. Should be passed in as a float (constant uncertainty throughout all examples), + or a numpy array of length ``N`` (estimated uncertainty for each example). + If not provided, this method will estimate the uncertainty as the sum of the epistemic and aleatoric uncertainty. + + save_space : + If True, then returned ``label_issues_df`` will not be stored as attribute. + This means some other methods like :py:meth:`self.get_label_issues ` will no longer work. + + coarse_search_range : + The coarse search range to find the value of ``k``, which estimates the fraction of data which have label issues. + More values represent a more thorough search (better expected results but longer runtimes). + + fine_search_size : + Size of fine-grained search grid to find the value of ``k``, which represents our estimate of the fraction of data which have label issues. + A higher number represents a more thorough search (better expected results but longer runtimes). + + + For info about the **other parameters**, see the docstring of :py:meth:`CleanLearning.fit() + `. + + Returns + ------- + label_issues_df : pd.DataFrame + DataFrame with info about label issues for each example. + Unless `save_space` argument is specified, same DataFrame is also stored as `self.label_issues_df` attribute accessible via + :py:meth:`get_label_issues`. + + Each row represents an example from our dataset and the DataFrame may contain the following columns: + + - *is_label_issue*: boolean mask for the entire dataset where ``True`` represents a label issue and ``False`` represents an example + that is accurately labeled with high confidence. + - *label_quality*: Numeric score that measures the quality of each label (how likely it is to be correct, + with lower scores indicating potentially erroneous labels). + - *given_label*: Values originally given for this example (same as `y` input). + - *predicted_label*: Values predicted by the trained model. + """ + + X, y = assert_valid_regression_inputs(X, y) + + if model_kwargs is None: + model_kwargs = {} + + if self.verbose: + print("Identifying label issues ...") + + # compute initial values to find best k + initial_predictions = self._get_cv_predictions(X, y, model_kwargs=model_kwargs) + initial_residual = initial_predictions - y + initial_sorted_index = np.argsort(abs(initial_residual)) + initial_r2 = r2_score(y, initial_predictions) + + self.k, r2 = self._find_best_k( + X=X, + y=y, + sorted_index=initial_sorted_index, + coarse_search_range=coarse_search_range, + fine_search_size=fine_search_size, + ) + + # check if initial r2 score (ie. not removing anything) is the best + if initial_r2 >= r2: + self.k = 0 + + # get predictions using the best k + predictions = self._get_cv_predictions( + X, y, sorted_index=initial_sorted_index, k=self.k, model_kwargs=model_kwargs + ) + residual = predictions - y + + if uncertainty is None: + epistemic_uncertainty = self.get_epistemic_uncertainty(X, y, predictions=predictions) + if self.include_aleatoric_uncertainty: + aleatoric_uncertainty = self.get_aleatoric_uncertainty(X, residual) + else: + aleatoric_uncertainty = 0 + uncertainty = epistemic_uncertainty + aleatoric_uncertainty + else: + if isinstance(uncertainty, np.ndarray) and len(y) != len(uncertainty): + raise ValueError( + "If uncertainty is passed in as an array, it must have the same length as y." + ) + + label_quality_scores = np.exp(-abs(residual) / (uncertainty + TINY_VALUE)) + + label_issues_mask = np.zeros(len(y), dtype=bool) + num_issues = math.ceil(len(y) * self.k) + issues_index = np.argsort(label_quality_scores)[:num_issues] + label_issues_mask[issues_index] = True + + # convert predictions to int if input is int + if y.dtype == int: + predictions = predictions.astype(int) + + label_issues_df = pd.DataFrame( + { + "is_label_issue": label_issues_mask, + "label_quality": label_quality_scores, + "given_label": y, + "predicted_label": predictions, + } + ) + + if self.verbose: + print(f"Identified {np.sum(label_issues_mask)} examples with label issues.") + + if not save_space: + if self.label_issues_df is not None and self.verbose: + print( + "Overwriting previously identified label issues stored at self.label_issues_df. " + "self.get_label_issues() will now return the newly identified label issues. " + ) + self.label_issues_df = label_issues_df + self.label_issues_mask = label_issues_df["is_label_issue"].to_numpy() + elif self.verbose: + print("Not storing label_issues as attributes since save_space was specified.") + + return label_issues_df + + def get_label_issues(self) -> Optional[pd.DataFrame]: + """ + Accessor, returns `label_issues_df` attribute if previously computed. + This ``pd.DataFrame`` describes the issues identified for each example (each row corresponds to an example). + For column definitions, see the documentation of + :py:meth:`CleanLearning.find_label_issues`. + + Returns + ------- + label_issues_df : pd.DataFrame + DataFrame with (precomputed) info about the label issues for each example. + """ + if self.label_issues_df is None: + warnings.warn( + "Label issues have not yet been computed. Run `self.find_label_issues()` or `self.fit()` first." + ) + return self.label_issues_df + + def get_epistemic_uncertainty( + self, + X: np.ndarray, + y: np.ndarray, + predictions: Optional[np.ndarray] = None, + ) -> np.ndarray: + """ + Compute the epistemic uncertainty of the regression model for each example. This uncertainty is estimated using the bootstrapped + variance of the model predictions. + + Parameters + ---------- + X : + Data features (i.e. training inputs for ML), typically an array of shape ``(N, ...)``, where N is the number of examples. + + y : + An array of shape ``(N,)`` of target values (dependant variables), where some values may be erroneous. + + predictions : + Model predicted values of y, will be used as an extra bootstrap iteration to calculate the variance. + + Returns + _______ + epistemic_uncertainty : np.ndarray + The estimated epistemic uncertainty for each example. + """ + X, y = assert_valid_regression_inputs(X, y) + + if self.n_boot == 0: # does not estimate epistemic uncertainty + return np.zeros(len(y)) + else: + bootstrap_predictions = np.zeros(shape=(len(y), self.n_boot)) + for i in range(self.n_boot): + bootstrap_predictions[:, i] = self._get_cv_predictions(X, y, cv_n_folds=2) + + # add a set of predictions from model that was already trained + if predictions is not None: + _, predictions = assert_valid_regression_inputs(X, predictions) + bootstrap_predictions = np.hstack( + [bootstrap_predictions, predictions.reshape(-1, 1)] + ) + + return np.sqrt(np.var(bootstrap_predictions, axis=1)) + + def get_aleatoric_uncertainty( + self, + X: np.ndarray, + residual: np.ndarray, + ) -> float: + """ + Compute the aleatoric uncertainty of the data. This uncertainty is estimated by predicting the standard deviation + of the regression error. + + Parameters + ---------- + X : + Data features (i.e. training inputs for ML), typically an array of shape ``(N, ...)``, where N is the number of examples. + + residual : + The difference between the given value and the model predicted value of each examples, ie. + `predictions - y`. + + Returns + _______ + aleatoric_uncertainty : float + The overall estimated aleatoric uncertainty for this dataset. + """ + X, residual = assert_valid_regression_inputs(X, residual) + residual_predictions = self._get_cv_predictions(X, residual) + return np.sqrt(np.var(residual_predictions)) + + def save_space(self): + """ + Clears non-sklearn attributes of this estimator to save space (in-place). + This includes the DataFrame attribute that stored label issues which may be large for big datasets. + You may want to call this method before deploying this model (i.e. if you just care about producing predictions). + After calling this method, certain non-prediction-related attributes/functionality will no longer be available + """ + if self.label_issues_df is None and self.verbose: + print("self.label_issues_df is already empty") + + self.label_issues_df = None + self.label_issues_mask = None + self.k = None + + if self.verbose: + print("Deleted non-sklearn attributes such as label_issues_df to save space.") + + def _get_cv_predictions( + self, + X: np.ndarray, + y: np.ndarray, + sorted_index: Optional[np.ndarray] = None, + k: float = 0, + *, + cv_n_folds: Optional[int] = None, + seed: Optional[int] = None, + model_kwargs: Optional[dict] = None, + ) -> np.ndarray: + """ + Helper method to get out-of-fold predictions using cross validation. + This method also allows us to filter out the bottom k percent of label errors before training the cross-validation models + (both ``sorted_index`` and ``k`` has to be provided for this). + + Parameters + ---------- + X : + Data features (i.e. training inputs for ML), typically an array of shape ``(N, ...)``, where N is the number of examples. + + y : + An array of shape ``(N,)`` of target values (dependant variables), where some values may be erroneous. + + sorted_index : + Index of each example sorted by their residuals in ascending order. + + k : + The fraction of examples to hold out from the training sets. Usually this is the fraction of examples that are + deemed to contain errors. + + """ + # set to default unless specified otherwise + if cv_n_folds is None: + cv_n_folds = self.cv_n_folds + + if model_kwargs is None: + model_kwargs = {} + + if k < 0 or k > 1: + raise ValueError("k must be a value between 0 and 1") + elif k == 0: + if sorted_index is None: + sorted_index = np.array(range(len(y))) + in_sample_idx = sorted_index + else: + if sorted_index is None: + # TODO: better error message + raise ValueError( + "You need to pass in the index sorted by prediction quality to use with k" + ) + num_to_drop = math.ceil(len(sorted_index) * k) + in_sample_idx = sorted_index[:-num_to_drop] + out_of_sample_idx = sorted_index[-num_to_drop:] + + X_out_of_sample = X[out_of_sample_idx] + out_of_sample_predictions = np.zeros(shape=[len(out_of_sample_idx), cv_n_folds]) + + if len(in_sample_idx) < cv_n_folds: + raise ValueError( + f"There are too few examples to conduct {cv_n_folds}-fold cross validation. " + "You can either reduce cv_n_folds for cross validation, or decrease k to exclude less data." + ) + + predictions = np.zeros(shape=len(y)) + + kf = KFold(n_splits=cv_n_folds, shuffle=True, random_state=seed) + + for k_split, (cv_train_idx, cv_holdout_idx) in enumerate(kf.split(in_sample_idx)): + try: + model_copy = sklearn.base.clone(self.model) # fresh untrained copy of the model + except Exception: + raise ValueError( + "`model` must be clonable via: sklearn.base.clone(model). " + "You can either implement instance method `model.get_params()` to produce a fresh untrained copy of this model, " + "or you can implement the cross-validation outside of cleanlab " + "and pass in the obtained `pred_probs` to skip cleanlab's internal cross-validation" + ) + + # map the index to the actual index in the original dataset + data_idx_train, data_idx_holdout = ( + in_sample_idx[cv_train_idx], + in_sample_idx[cv_holdout_idx], + ) + + X_train_cv, X_holdout_cv, y_train_cv, y_holdout_cv = train_val_split( + X, y, data_idx_train, data_idx_holdout + ) + + model_copy.fit(X_train_cv, y_train_cv, **model_kwargs) + predictions_cv = model_copy.predict(X_holdout_cv) + + predictions[data_idx_holdout] = predictions_cv + + if k != 0: + out_of_sample_predictions[:, k_split] = model_copy.predict(X_out_of_sample) + + if k != 0: + out_of_sample_predictions_avg = np.mean(out_of_sample_predictions, axis=1) + predictions[out_of_sample_idx] = out_of_sample_predictions_avg + + return predictions + + def _find_best_k( + self, + X: np.ndarray, + y: np.ndarray, + sorted_index: np.ndarray, + coarse_search_range: list = [0.01, 0.05, 0.1, 0.15, 0.2], + fine_search_size: int = 3, + ) -> Tuple[float, float]: + """ + Helper method that conducts a coarse and fine grained grid search to determine the best value + of k, the fraction of the dataset that contains issues. + + Returns a tuple containing the the best value of k (ie. the one that has the best r squared score), + and the corrsponding r squared score obtained when dropping k% of the data. + """ + if len(coarse_search_range) == 0: + raise ValueError("coarse_search_range must have at least 1 value of k") + elif len(coarse_search_range) == 1: + curr_k = coarse_search_range[0] + num_examples_kept = math.floor(len(y) * (1 - curr_k)) + if num_examples_kept < self.cv_n_folds: + raise ValueError( + f"There are too few examples to conduct {self.cv_n_folds}-fold cross validation. " + "You can either reduce self.cv_n_folds for cross validation, or decrease k to exclude less data." + ) + predictions = self._get_cv_predictions( + X=X, + y=y, + sorted_index=sorted_index, + k=curr_k, + ) + best_r2 = r2_score(y, predictions) + best_k = coarse_search_range[0] + else: + # conduct coarse search + coarse_search_range = sorted(coarse_search_range) # sort to conduct fine search well + r2_coarse = np.full(len(coarse_search_range), np.NaN) + for i in range(len(coarse_search_range)): + curr_k = coarse_search_range[i] + num_examples_kept = math.floor(len(y) * (1 - curr_k)) + # check if there are too few examples to do cross val + if num_examples_kept < self.cv_n_folds: + r2_coarse[i] = -1e30 # arbitrary large negative number + else: + predictions = self._get_cv_predictions( + X=X, + y=y, + sorted_index=sorted_index, + k=curr_k, + ) + r2_coarse[i] = r2_score(y, predictions) + + max_r2_ind = np.argmax(r2_coarse) + + # conduct fine search + if fine_search_size < 0: + raise ValueError("fine_search_size must at least 0") + elif fine_search_size == 0: + best_k = coarse_search_range[np.argmax(r2_coarse)] + best_r2 = np.max(r2_coarse) + else: + fine_search_range = np.array([]) + if max_r2_ind != 0: + fine_search_range = np.append( + np.linspace( + coarse_search_range[max_r2_ind - 1], + coarse_search_range[max_r2_ind], + fine_search_size + 1, + endpoint=False, + )[1:], + fine_search_range, + ) + if max_r2_ind != len(coarse_search_range) - 1: + fine_search_range = np.append( + fine_search_range, + np.linspace( + coarse_search_range[max_r2_ind], + coarse_search_range[max_r2_ind + 1], + fine_search_size + 1, + endpoint=False, + )[1:], + ) + + r2_fine = np.full(len(fine_search_range), np.NaN) + for i in range(len(fine_search_range)): + curr_k = fine_search_range[i] + num_examples_kept = math.floor(len(y) * (1 - curr_k)) + # check if there are too few examples to do cross val + if num_examples_kept < self.cv_n_folds: + r2_fine[i] = -1e30 # arbitrary large negative number + else: + predictions = self._get_cv_predictions( + X=X, + y=y, + sorted_index=sorted_index, + k=curr_k, + ) + r2_fine[i] = r2_score(y, predictions) + + # check the max between coarse and fine search + if max(r2_coarse) > max(r2_fine): + best_k = coarse_search_range[np.argmax(r2_coarse)] + best_r2 = np.max(r2_coarse) + else: + best_k = fine_search_range[np.argmax(r2_fine)] + best_r2 = np.max(r2_fine) + + return best_k, best_r2 + + def _process_label_issues_arg( + self, + label_issues: Union[pd.DataFrame, pd.Series, np.ndarray], + y: LabelLike, + ) -> pd.DataFrame: + """ + Helper method to process the label_issues input into a well-formatted DataFrame. + """ + y = labels_to_array(y) + + if isinstance(label_issues, pd.DataFrame): + if "is_label_issue" not in label_issues.columns: + raise ValueError( + "DataFrame label_issues must contain column: 'is_label_issue'. " + "See CleanLearning.fit() documentation for label_issues column descriptions." + ) + if len(label_issues) != len(y): + raise ValueError("label_issues and labels must have same length") + if "given_label" in label_issues.columns and np.any( + label_issues["given_label"].to_numpy() != y + ): + raise ValueError("labels must match label_issues['given_label']") + return label_issues + + elif isinstance(label_issues, (pd.Series, np.ndarray)): + if label_issues.dtype is not np.dtype("bool"): + raise ValueError("If label_issues is numpy.array, dtype must be 'bool'.") + if label_issues.shape != y.shape: + raise ValueError("label_issues must have same shape as labels") + return pd.DataFrame({"is_label_issue": label_issues, "given_label": y}) + + else: + raise ValueError( + "label_issues must be either pandas.DataFrame, pandas.Series or numpy.ndarray" + ) diff --git a/cleanlab/regression/rank.py b/cleanlab/regression/rank.py new file mode 100644 index 0000000000..d73efdfba2 --- /dev/null +++ b/cleanlab/regression/rank.py @@ -0,0 +1,186 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . + + +""" +Methods to score the quality of each label in a regression dataset. These can be used to rank the examples whose Y-value is most likely erroneous. + +Note: Label quality scores are most accurate when they are computed based on out-of-sample `predictions` from your regression model. +To obtain out-of-sample predictions for every datapoint in your dataset, you can use :ref:`cross-validation `. This is encouraged to get better results. + +If you have a sklearn-compatible regression model, consider using `cleanlab.regression.learn.CleanLearning` instead, which can more accurately identify noisy label values. +""" + +from typing import Dict, Callable +import numpy as np +from numpy.typing import ArrayLike +from sklearn.neighbors import NearestNeighbors + +from cleanlab.outlier import OutOfDistribution +from cleanlab.internal.regression_utils import assert_valid_prediction_inputs + +from cleanlab.internal.constants import TINY_VALUE + + +def get_label_quality_scores( + labels: ArrayLike, + predictions: ArrayLike, + *, + method: str = "outre", +) -> np.ndarray: + """ + Returns label quality score for each example in the regression dataset. + + Each score is a continous value in the range [0,1] + + * 1 - clean label (given label is likely correct). + * 0 - dirty label (given label is likely incorrect). + + Parameters + ---------- + labels : array_like + Raw labels from original dataset. + 1D array of shape ``(N, )`` containing the given labels for each example (aka. Y-value, response/target/dependent variable), where N is number of examples in the dataset. + + predictions : np.ndarray + 1D array of shape ``(N,)`` containing the predicted label for each example in the dataset. These should be out-of-sample predictions from a trained regression model, which you can obtain for every example in your dataset via :ref:`cross-validation `. + + method : {"residual", "outre"}, default="outre" + String specifying which method to use for scoring the quality of each label and identifying which labels appear most noisy. + + Returns + ------- + label_quality_scores: + Array of shape ``(N, )`` of scores between 0 and 1, one per example in the dataset. + + Lower scores indicate examples more likely to contain a label issue. + + Examples + -------- + >>> import numpy as np + >>> from cleanlab.regression.rank import get_label_quality_scores + >>> labels = np.array([1,2,3,4]) + >>> predictions = np.array([2,2,5,4.1]) + >>> label_quality_scores = get_label_quality_scores(labels, predictions) + >>> label_quality_scores + array([0.00323821, 0.33692597, 0.00191686, 0.33692597]) + """ + + # Check if inputs are valid + labels, predictions = assert_valid_prediction_inputs( + labels=labels, predictions=predictions, method=method + ) + + scoring_funcs: Dict[str, Callable[[np.ndarray, np.ndarray], np.ndarray]] = { + "residual": _get_residual_score_for_each_label, + "outre": _get_outre_score_for_each_label, + } + + scoring_func = scoring_funcs.get(method, None) + if not scoring_func: + raise ValueError( + f""" + {method} is not a valid scoring method. + Please choose a valid scoring technique: {scoring_funcs.keys()}. + """ + ) + + # Calculate scores + label_quality_scores = scoring_func(labels, predictions) + return label_quality_scores + + +def _get_residual_score_for_each_label( + labels: np.ndarray, + predictions: np.ndarray, +) -> np.ndarray: + """Returns a residual label-quality score for each example. + + This is function to compute label-quality scores for regression datasets, + where lower score indicate labels less likely to be correct. + + Residual based scores can work better for datasets where independent variables + are based out of normal distribution. + + Parameters + ---------- + labels: np.ndarray + Labels in the same format expected by the :py:func:`get_label_quality_scores ` function. + + predictions: np.ndarray + Predicted labels in the same format expected by the :py:func:`get_label_quality_scores ` function. + + Returns + ------- + label_quality_scores: np.ndarray + Contains one score (between 0 and 1) per example. + Lower scores indicate more likely mislabled examples. + + """ + residual = predictions - labels + label_quality_scores = np.exp(-abs(residual)) + return label_quality_scores + + +def _get_outre_score_for_each_label( + labels: np.ndarray, + predictions: np.ndarray, + *, + residual_scale: float = 5, + frac_neighbors: float = 0.5, + neighbor_metric: str = "euclidean", +) -> np.ndarray: + """Returns OUTRE based label-quality scores. + + This function computes label-quality scores for regression datasets, + where a lower score indicates labels that are less likely to be correct. + + Parameters + ---------- + labels: np.ndarray + Labels in the same format as expected by the :py:func:`get_label_quality_scores ` function. + + predictions: np.ndarray + Predicted labels in the same format as expected by the :py:func:`get_label_quality_scores ` function. + + residual_scale: float, default = 5 + Multiplicative factor to adjust scale (standard deviation) of the residuals relative to the labels. + + frac_neighbors: float, default = 0.5 + Fraction of examples in dataset that should be considered as `n_neighbors` in the ``NearestNeighbors`` object used internally to assess outliers. + + neighbor_metric: str, default = "euclidean" + The parameter is passed to sklearn NearestNeighbors. # TODO add reference to sklearn.NearestNeighbor? + + Returns + ------- + label_quality_scores: np.ndarray + Contains one score (between 0 and 1) per example. + Lower scores indicate more likely mislabled examples. + """ + residual = predictions - labels + labels = (labels - labels.mean()) / (labels.std() + TINY_VALUE) + residual = residual_scale * ((residual - residual.mean()) / (residual.std() + TINY_VALUE)) + + # 2D features by combining labels and residual + features = np.array([labels, residual]).T + + neighbors = int(np.ceil(frac_neighbors * labels.shape[0])) + knn = NearestNeighbors(n_neighbors=neighbors, metric=neighbor_metric).fit(features) + ood = OutOfDistribution(params={"knn": knn}) + + label_quality_scores = ood.score(features=features) + return label_quality_scores diff --git a/cleanlab/segmentation/__init__.py b/cleanlab/segmentation/__init__.py new file mode 100644 index 0000000000..fbc2eb7eac --- /dev/null +++ b/cleanlab/segmentation/__init__.py @@ -0,0 +1,3 @@ +from . import rank +from . import filter +from . import summary diff --git a/cleanlab/segmentation/filter.py b/cleanlab/segmentation/filter.py new file mode 100644 index 0000000000..9c93409dfd --- /dev/null +++ b/cleanlab/segmentation/filter.py @@ -0,0 +1,168 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . + +""" +Methods to find label issues in image semantic segmentation datasets, where each pixel in an image receives its own class label. + +""" + +from cleanlab.experimental.label_issues_batched import find_label_issues_batched +import numpy as np +from typing import Tuple, Optional + +from cleanlab.internal.segmentation_utils import _get_valid_optional_params, _check_input + + +def find_label_issues( + labels: np.ndarray, + pred_probs: np.ndarray, + *, + batch_size: Optional[int] = None, + n_jobs: Optional[int] = None, + verbose: bool = True, + **kwargs, +) -> np.ndarray: + """ + Returns a boolean mask for the entire dataset, per pixel where ``True`` represents + an example identified with a label issue and ``False`` represents an example of a pixel correctly labeled. + + * N - Number of images in the dataset + * K - Number of classes in the dataset + * H - Height of each image + * W - Width of each image + + Tip: if you encounter the error "pred_probs is not defined", try setting ``n_jobs=1``. + + Parameters + ---------- + labels: + A discrete array of shape ``(N,H,W,)`` of noisy labels for a semantic segmentation dataset, i.e. some labels may be erroneous. + *Format requirements*: for a dataset with K classes, each pixel must be labeled using an integer in 0, 1, ..., K-1. + Tip: If your labels are one hot encoded you can do: ``labels = np.argmax(labels_one_hot,axis=1)`` assuming that `labels_one_hot` is of dimension ``(N,K,H,W)``, in order to get properly formatted `labels` + + pred_probs: + An array of shape ``(N,K,H,W,)`` of model-predicted class probabilities, + ``P(label=k|x)`` for each pixel ``x``. The prediction for each pixel is an array corresponding to the estimated likelihood that this pixel belongs to each of the ``K`` classes. The 2nd dimension of `pred_probs` must be ordered such that these probabilities correspond to class 0, 1, ..., K-1. + + batch_size: + Optional size of image mini-batches used for computing the label issues in a streaming fashion (does not affect results, just the runtime and memory requirements). + To maximize efficiency, try to use the largest `batch_size` your memory allows. If not provided, a good default is used. + + n_jobs: + Optional number of processes for multiprocessing (default value = 1). Only used on Linux. + If `n_jobs=None`, will use either the number of: physical cores if psutil is installed, or logical cores otherwise. + + verbose: + Set to ``False`` to suppress all print statements. + + **kwargs: + * downsample: int, + Optional factor to shrink labels and pred_probs by. Default ``1`` + Must be a factor divisible by both the labels and the pred_probs. Larger values of `downsample` produce faster runtimes but potentially less accurate results due to over-compression. Set to 1 to avoid any downsampling. + + Returns + ------- + label_issues: np.ndarray + Returns a boolean **mask** for the entire dataset of length `(N,H,W)` + where ``True`` represents a pixel label issue and ``False`` represents an example that is correctly labeled. + """ + batch_size, n_jobs = _get_valid_optional_params(batch_size, n_jobs) + downsample = kwargs.get("downsample", 1) + + def downsample_arrays( + labels: np.ndarray, pred_probs: np.ndarray, factor: int = 1 + ) -> Tuple[np.ndarray, np.ndarray]: + if factor == 1: + return labels, pred_probs + + num_image, num_classes, h, w = pred_probs.shape + + # Check if possible to downsample + if h % downsample != 0 or w % downsample != 0: + raise ValueError( + f"Height {h} and width {w} not divisible by downsample value of {downsample}. Set kwarg downsample to 1 to avoid downsampling." + ) + small_labels = np.round( + labels.reshape((num_image, h // factor, factor, w // factor, factor)).mean(4).mean(2) + ) + small_pred_probs = ( + pred_probs.reshape((num_image, num_classes, h // factor, factor, w // factor, factor)) + .mean(5) + .mean(3) + ) + + # We want to make sure that pred_probs are renormalized + row_sums = small_pred_probs.sum(axis=1) + renorm_small_pred_probs = small_pred_probs / np.expand_dims(row_sums, 1) + + return small_labels, renorm_small_pred_probs + + def flatten_and_preprocess_masks( + labels: np.ndarray, pred_probs: np.ndarray + ) -> Tuple[np.ndarray, np.ndarray]: + _, num_classes, _, _ = pred_probs.shape + labels_flat = labels.flatten().astype(int) + pred_probs_flat = np.moveaxis(pred_probs, 0, 1).reshape(num_classes, -1) + + return labels_flat, pred_probs_flat.T + + ## + _check_input(labels, pred_probs) + + # Added Downsampling + pre_labels, pre_pred_probs = downsample_arrays(labels, pred_probs, downsample) + + num_image, num_classes, h, w = pre_pred_probs.shape + # flatten images just preps labels and pred_probs + + pre_labels, pre_pred_probs = flatten_and_preprocess_masks(pre_labels, pre_pred_probs) + + ranked_label_issues = find_label_issues_batched( + pre_labels, pre_pred_probs, batch_size=batch_size, n_jobs=n_jobs, verbose=verbose + ) + + # Finding the right indicies + relative_index = ranked_label_issues % (h * w) + pixel_coor_i, pixel_coor_j = np.unravel_index(relative_index, (h, w)) + image_number = ranked_label_issues // (h * w) + + # Upsample carefully maintaining indicies + label_issues = np.full((num_image, h, w), False) + + for num, ii, jj in zip(image_number, pixel_coor_i, pixel_coor_j): + # only want to call it an error if pred_probs doesnt match the label at that pixel + label_issues[num, ii, jj] = True + if downsample == 1: + # check if pred_probs matches the label at that pixel + if np.argmax(pred_probs[num, :, ii, jj]) == labels[num, ii, jj]: + label_issues[num, ii, jj] = False + + if downsample != 1: + label_issues = label_issues.repeat(downsample, axis=1).repeat(downsample, axis=2) + + for num, ii, jj in zip(image_number, pixel_coor_i, pixel_coor_j): + # Upsample the coordinates + upsampled_ii = ii * downsample + upsampled_jj = jj * downsample + # Iterate over the upsampled region + for row in range(upsampled_ii, upsampled_ii + downsample): + for col in range(upsampled_jj, upsampled_jj + downsample): + # Check if the predicted class (argmax) at the identified issue location matches the true label + if np.argmax(pred_probs[num, :, row, col]) == labels[num, row, col]: + # If they match, set the corresponding entry in the label_issues array to False + label_issues[num, row, col] = False + + return label_issues diff --git a/cleanlab/segmentation/rank.py b/cleanlab/segmentation/rank.py new file mode 100644 index 0000000000..2d130e8b32 --- /dev/null +++ b/cleanlab/segmentation/rank.py @@ -0,0 +1,246 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . + +""" +Methods to rank and score images in a semantic segmentation dataset based on how likely they are to contain mislabeled pixels. +""" +import numpy as np +import warnings +from typing import Optional, Union, Tuple +from cleanlab.segmentation.filter import find_label_issues + +from cleanlab.internal.segmentation_utils import _get_valid_optional_params, _check_input + + +def get_label_quality_scores( + labels: np.ndarray, + pred_probs: np.ndarray, + *, + method: str = "softmin", + batch_size: Optional[int] = None, + n_jobs: Optional[int] = None, + verbose: bool = True, + **kwargs, +) -> Tuple[np.ndarray, np.ndarray]: + """Returns a label quality score for each image. + + This is a function to compute label quality scores for semantic segmentation datasets, + where lower scores indicate labels less likely to be correct. + + * N - Number of images in the dataset + * K - Number of classes in the dataset + * H - Height of each image + * W - Width of each image + + Parameters + ---------- + labels: + A discrete array of noisy labels for a segmantic segmentation dataset, in the shape``(N,H,W,)``. + where each pixel must be integer in 0, 1, ..., K-1. + Refer to documentation for this argument in :py:func:find_label_issues for further details. + + pred_probs: + An array of shape ``(N,K,H,W,)`` of model-predicted class probabilities. + Refer to documentation for this argument in :py:func:find_label_issues for further details. + + method: {"softmin", "num_pixel_issues"}, default="softmin" + Label quality scoring method. + - "softmin" - Calculates the inner product between scores and softmax(1-scores). For efficiency, use instead of "num_pixel_issues". + - "num_pixel_issues" - Uses the number of pixels with label issues for each image using :py:func:find_label_issues + + batch_size : + Optional size of mini-batches to use for estimating the label issues for 'num_pixel_issues' only, not 'softmin' + To maximize efficiency, try to use the largest `batch_size` your memory allows. If not provided, a good default is used. + + n_jobs: + Optional number of processes for multiprocessing (default value = 1). Only used on Linux. For 'num_pixel_issues' only, not 'softmin' + If `n_jobs=None`, will use either the number of: physical cores if psutil is installed, or logical cores otherwise. + + verbose: + Set to ``False`` to suppress all print statements. + + **kwargs: + * downsample : int, + Factor to shrink labels and pred_probs by for 'num_pixel_issues' only, not 'softmin' . Default ``16`` + Must be a factor divisible by both the labels and the pred_probs. Larger values of `downsample` produce faster runtimes but potentially less accurate results due to over-compression. Set to 1 to avoid any downsampling. + * temperature : float, + Temperature for softmin. Default ``0.1`` + + + Returns + ------- + image_scores: + Array of shape ``(N, )`` of scores between 0 and 1, one per image in the dataset. + Lower scores indicate image more likely to contain a label issue. + pixel_scores: + Array of shape ``(N,H,W)`` of scores between 0 and 1, one per pixel in the dataset. + """ + batch_size, n_jobs = _get_valid_optional_params(batch_size, n_jobs) + _check_input(labels, pred_probs) + + softmin_temperature = kwargs.get("temperature", 0.1) + downsample_num_pixel_issues = kwargs.get("downsample", 1) + + if method == "num_pixel_issues": + _, K, _, _ = pred_probs.shape + labels_expanded = labels[:, np.newaxis, :, :] + mask = np.arange(K)[np.newaxis, :, np.newaxis, np.newaxis] == labels_expanded + # Calculate pixel_scores + masked_pred_probs = np.where(mask, pred_probs, 0) + pixel_scores = masked_pred_probs.sum(axis=1) + scores = find_label_issues( + labels, + pred_probs, + downsample=downsample_num_pixel_issues, + n_jobs=n_jobs, + verbose=verbose, + batch_size=batch_size, + ) + img_scores = 1 - np.mean(scores, axis=(1, 2)) + return (img_scores, pixel_scores) + + if downsample_num_pixel_issues != 1: + warnings.warn( + f"image will not downsample for method {method} is only for method: num_pixel_issues" + ) + + num_im, num_class, h, w = pred_probs.shape + image_scores = [] + pixel_scores = [] + if verbose: + from tqdm.auto import tqdm + + pbar = tqdm(desc=f"images processed using {method}", total=num_im) + for image in range(num_im): + image_probs = pred_probs[image][ + labels[image], + np.arange(h)[:, None], + np.arange(w), + ] + pixel_scores.append(image_probs) + image_scores.append( + _get_label_quality_per_image( + np.array(image_probs.flatten()), method=method, temperature=softmin_temperature + ) + ) + if verbose: + pbar.update(1) + return np.array(image_scores), np.array(pixel_scores) + + +def issues_from_scores( + image_scores: np.ndarray, pixel_scores: Optional[np.ndarray] = None, threshold: float = 0.1 +) -> Union[list, np.ndarray]: + """ + Converts scores output by :py:func:`segmentation.rank.get_label_quality_scores ` + to a list of issues of similar format as output by :py:func:`segmentation.filter.find_label_issues `. + + Issues are sorted by label quality score, from most to least severe. + + Only considers as issues those tokens with label quality score lower than `threshold`, + so this parameter determines the number of issues that are returned. + + Note: This method is intended for converting the most severely mislabeled examples to a format compatible with + ``summary`` methods like :py:func:`segmentation.summary.display_issues `. + This method does not estimate the number of label errors since the `threshold` is arbitrary, + for that instead use :py:func:`segmentation.filter.find_label_issues `, + which estimates the label errors via Confident Learning rather than score thresholding. + + Parameters + ---------- + image_scores: + Array of shape `(N, )` of overall image scores, where `N` is the number of images in the dataset. + Same format as the `image_scores` returned by :py:func:`segmentation.rank.get_label_quality_scores `. + + pixel_scores: + Optional array of shape ``(N,H,W)`` of scores between 0 and 1, one per pixel in the dataset. + Same format as the `pixel_scores` returned by :py:func:`segmentation.rank.get_label_quality_scores `. + + threshold: + Optional quality scores threshold that determines which pixels are included in result. Pixels with with quality scores above the `threshold` are not + included in the result. If not provided, all pixels are included in result. + + Returns + --------- + issues: + Returns a boolean **mask** for the entire dataset + where ``True`` represents a pixel label issue and ``False`` represents an example that is + accurately labeled with using the threshold provided by the user. + Use :py:func:`segmentation.summary.display_issues ` + to view these issues within the original images. + + If `pixel_scores` is not provided, returns array of integer indices (rather than boolean mask) of the images whose label quality score + falls below the `threshold` (also sorted by overall label quality score of each image). + + """ + + if image_scores is None: + raise ValueError("pixel_scores must be provided") + if threshold < 0 or threshold > 1 or threshold is None: + raise ValueError("threshold must be between 0 and 1") + + if pixel_scores is not None: + issues = np.where(pixel_scores < threshold, True, False) + else: + ranking = np.argsort(image_scores) + cutoff = np.searchsorted(image_scores[ranking], threshold) + issues = ranking[: cutoff + 1] + return issues + + +def _get_label_quality_per_image(pixel_scores, method=None, temperature=0.1): + from cleanlab.internal.multilabel_scorer import softmin + + """ + Input pixel scores and get label quality score for that image, currently using the "softmin" method. + + Parameters + ---------- + pixel_scores: + Per-pixel label quality scores in flattened array of shape ``(N, )``, where N is the number of pixels in the image. + + method: default "softmin" + Method to use to calculate the image's label quality score. + Currently only supports "softmin". + temperature: default 0.1 + Temperature of the softmax function. Too small values may cause numerical underflow and NaN scores. + + Lower values encourage this method to converge toward the label quality score of the pixel with the lowest quality label in the image. + + Higher values encourage this method to converge toward the average label quality score of all pixels in the image. + + Returns + --------- + image_score: + Float of the image's label quality score from 0 to 1, 0 being the lowest quality and 1 being the highest quality. + + """ + if pixel_scores is None or pixel_scores.size == 0: + raise Exception("Invalid Input: pixel_scores cannot be None or an empty list") + + if temperature == 0 or temperature is None: + raise Exception("Invalid Input: temperature cannot be zero or None") + + pixel_scores_64 = pixel_scores.astype("float64") + if method == "softmin": + if len(pixel_scores_64) > 0: + return softmin( + np.expand_dims(pixel_scores_64, axis=0), axis=1, temperature=temperature + )[0] + else: + raise Exception("Invalid Input: pixel_scores is empty") + else: + raise Exception("Invalid Method: Specify correct method. Currently only supports 'softmin'") diff --git a/cleanlab/segmentation/summary.py b/cleanlab/segmentation/summary.py new file mode 100644 index 0000000000..c578f71cd3 --- /dev/null +++ b/cleanlab/segmentation/summary.py @@ -0,0 +1,354 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . + +""" +Methods to display images and their label issues in a semantic segmentation dataset, as well as summarize the overall types of issues identified. +""" + +from typing import Any, Dict, List, Optional + +import numpy as np +import pandas as pd +from tqdm import tqdm + +from cleanlab.internal.segmentation_utils import _get_summary_optional_params + + +def display_issues( + issues: np.ndarray, + *, + labels: Optional[np.ndarray] = None, + pred_probs: Optional[np.ndarray] = None, + class_names: Optional[List[str]] = None, + exclude: Optional[List[int]] = None, + top: Optional[int] = None, +) -> None: + """ + Display semantic segmentation label issues, showing images with problematic pixels highlighted. + + Can also show given and predicted masks for each image identified to have label issue. + + Parameters + ---------- + issues: + Boolean **mask** for the entire dataset + where ``True`` represents a pixel label issue and ``False`` represents an example that is + accurately labeled. + + Same format as output by :py:func:`segmentation.filter.find_label_issues ` + or :py:func:`segmentation.rank.issues_from_scores `. + + labels: + Optional discrete array of noisy labels for a segmantic segmentation dataset, in the shape``(N,H,W,)``. + where each pixel must be integer in 0, 1, ..., K-1. + If `labels` is provided, this function also displays given label of the pixel identified with issue. + Refer to documentation for this argument in :py:func:find_label_issues for more information. + + pred_probs: + Optional array of shape ``(N,K,H,W,)`` of model-predicted class probabilities. + If `pred_probs` is provided, this function also displays predicted label of the pixel identified with issue. + Refer to documentation for this argument in :py:func:find_label_issues for more information. + + Tip: If your labels are one hot encoded you can `np.argmax(labels_one_hot,axis=1)` assuming that `labels_one_hot` is of dimension (N,K,H,W) + before entering in the function + + class_names: + Optional list of strings, where each string represents the name of a class in the semantic segmentation problem. + The order of the names should correspond to the numerical order of the classes. The list length should be + equal to the number of unique classes present in the labels. + If provided, this function will generate a legend + showing the color mapping of each class in the provided colormap. + + Example: + If there are three classes in your labels, represented by 0, 1, 2, then class_names might look like this: + class_names = ['background', 'person', 'dog'] + + top: + Optional maximum number of issues to be printed. If not provided, a good default is used. + + exclude: + Optional list of label classes that can be ignored in the errors, each element must be 0, 1, ..., K-1 + + """ + class_names, exclude, top = _get_summary_optional_params(class_names, exclude, top) + if labels is None and len(exclude) > 0: + raise ValueError("Provide labels to allow class exclusion") + + top = min(top, len(issues)) + + correct_ordering = np.argsort(-np.sum(issues, axis=(1, 2)))[:top] + + try: + import matplotlib.pyplot as plt + import matplotlib.patches as mpatches + except: + raise ImportError('try "pip install matplotlib"') + + output_plots = (pred_probs is not None) + (labels is not None) + 1 + + # Colormap for errors + error_cmap = plt.cm.colors.ListedColormap(["none", "red"]) + _, h, w = issues.shape + if output_plots > 1: + if pred_probs is not None: + _, num_classes, _, _ = pred_probs.shape + cmap = _generate_colormap(num_classes) + elif labels is not None: + num_classes = max(np.unique(labels)) + 1 + cmap = _generate_colormap(num_classes) + else: + cmap = None + + # Show a legend + if class_names is not None and cmap is not None: + patches = [ + mpatches.Patch(color=cmap[i], label=class_names[i]) for i in range(len(class_names)) + ] + legend = plt.figure() # adjust figsize for larger legend + legend.legend( + handles=patches, loc="center", ncol=len(class_names), facecolor="white", fontsize=20 + ) # adjust fontsize for larger text + plt.axis("off") + plt.show() + + for i in correct_ordering: + # Show images + fig, axes = plt.subplots(1, output_plots, figsize=(5 * output_plots, 5)) + plot_index = 0 + + # First image - Given truth labels + if labels is not None: + axes[plot_index].imshow(cmap[labels[i]]) + axes[plot_index].set_title("Given Labels") + plot_index += 1 + + # Second image - Argmaxed pred_probs + if pred_probs is not None: + axes[plot_index].imshow(cmap[np.argmax(pred_probs[i], axis=0)]) + axes[plot_index].set_title("Argmaxed Prediction Probabilities") + plot_index += 1 + + # Third image - Errors + if output_plots == 1: + ax = axes + else: + ax = axes[plot_index] + + mask = np.full((h, w), True) + if labels is not None and len(exclude) != 0: + mask = ~np.isin(labels[i], exclude) + ax.imshow(issues[i] & mask, cmap=error_cmap, vmin=0, vmax=1) + ax.set_title(f"Image {i}: Suggested Errors (in Red)") + plt.show() + + return None + + +def common_label_issues( + issues: np.ndarray, + labels: np.ndarray, + pred_probs: np.ndarray, + *, + class_names: Optional[List[str]] = None, + exclude: Optional[List[int]] = None, + top: Optional[int] = None, + verbose: bool = True, +) -> pd.DataFrame: + """ + Display the frequency of which label are swapped in the dataset. + + These may correspond to pixels that are ambiguous or systematically misunderstood by the data annotators. + + * N - Number of images in the dataset + * K - Number of classes in the dataset + * H - Height of each image + * W - Width of each image + + Parameters + ---------- + issues: + Boolean **mask** for the entire dataset + where ``True`` represents a pixel label issue and ``False`` represents an example that is + accurately labeled. + + Same format as output by :py:func:`segmentation.filter.find_label_issues ` + or :py:func:`segmentation.rank.issues_from_scores `. + + labels: + A discrete array of noisy labels for a segmantic segmentation dataset, in the shape``(N,H,W,)``. + where each pixel must be integer in 0, 1, ..., K-1. + Refer to documentation for this argument in :py:func:`find_label_issues ` for more information. + + pred_probs: + An array of shape ``(N,K,H,W,)`` of model-predicted class probabilities. + Refer to documentation for this argument in :py:func:`find_label_issues ` for more information. + + Tip: If your labels are one hot encoded you can `np.argmax(labels_one_hot,axis=1)` assuming that `labels_one_hot` is of dimension (N,K,H,W) + before entering in the function + + class_names: + Optional length K list of names of each class, such that `class_names[i]` is the string name of the class corresponding to `labels` with value `i`. + If `class_names` is provided, display these string names for predicted and given labels, otherwise display the integer index of classes. + + exclude: + Optional list of label classes that can be ignored in the errors, each element must be in 0, 1, ..., K-1. + + top: + Optional maximum number of tokens to print information for. If not provided, a good default is used. + + verbose: + Set to ``False`` to suppress all print statements. + + Returns + ------- + issues_df: + DataFrame `issues_df` contains columns ``['given_label', 'predicted_label', 'num_label_issues']`` and each row contains information for a + given/predicted label swap, ordered by the number of label issues inferred for this type of label swap. + """ + try: + N, K, H, W = pred_probs.shape + except: + raise ValueError("pred_probs must be of shape (N, K, H, W)") + + assert labels.shape == (N, H, W), "labels must be of shape (N, H, W)" + + class_names, exclude, top = _get_summary_optional_params(class_names, exclude, top) + # Find issues by pixel coordinates + issue_coords = np.column_stack(np.where(issues)) + + # Count issues per class (given label) + count: Dict[int, Any] = {} + for i, j, k in tqdm(issue_coords): + label = labels[i, j, k] + pred = pred_probs[i, :, j, k].argmax() + if label not in count: + count[label] = np.zeros(K, dtype=int) + if pred not in exclude: + count[label][pred] += 1 + + # Prepare output DataFrame + if class_names is None: + class_names = [str(i) for i in range(K)] + + info = [] + for given_label, class_name in enumerate(class_names): + if given_label in count: + for pred_label, num_issues in enumerate(count[given_label]): + if num_issues > 0: + info.append([class_name, class_names[pred_label], num_issues]) + + info = sorted(info, key=lambda x: x[2], reverse=True)[:top] + issues_df = pd.DataFrame(info, columns=["given_label", "predicted_label", "num_pixel_issues"]) + + if verbose: + for idx, row in issues_df.iterrows(): + print( + f"Class '{row['given_label']}' is potentially mislabeled as class for '{row['predicted_label']}' " + f"{row['num_pixel_issues']} pixels in the dataset" + ) + + return issues_df + + +def filter_by_class( + class_index: int, issues: np.ndarray, labels: np.ndarray, pred_probs: np.ndarray +) -> np.ndarray: + """ + Return label issues involving particular class. Note that this includes errors where the given label is the class of interest, and the predicted label is any other class. + + Parameters + ---------- + class_index: + The specific class you are interested in. + + issues: + Boolean **mask** for the entire dataset where ``True`` represents a pixel label issue and ``False`` represents an example that is + accurately labeled. + + Same format as output by :py:func:`segmentation.filter.find_label_issues ` + or :py:func:`segmentation.rank.issues_from_scores `. + + labels: + A discrete array of noisy labels for a segmantic segmentation dataset, in the shape``(N,H,W,)``. + where each pixel must be integer in 0, 1, ..., K-1. + Refer to documentation for this argument in :py:func:`find_label_issues ` for further details. + + pred_probs: + An array of shape ``(N,K,H,W,)`` of model-predicted class probabilities. + Refer to documentation for this argument in :py:func:`find_label_issues ` for further details. + + Returns + ---------- + issues_subset: + Boolean **mask** for the subset dataset where ``True`` represents a pixel label issue and ``False`` represents an example that is + accurately labeled for the labeled class. + + Returned mask shows **all** instances that involve the particular class of interest. + + + """ + issues_subset = (issues & np.isin(labels, class_index)) | ( + issues & np.isin(pred_probs.argmax(1), class_index) + ) + return issues_subset + + +def _generate_colormap(num_colors): + """ + Finds a unique color map based on the number of colors inputted ideal for semantic segmentation. + Parameters + ---------- + num_colors: + How many unique colors you want + + Returns + ------- + colors: + colors with num_colors distinct colors + """ + + try: + from matplotlib.cm import hsv + except: + raise ImportError('try "pip install matplotlib"') + + num_shades = 7 + num_colors_with_shades = -(-num_colors // num_shades) * num_shades + linear_nums = np.linspace(0, 1, num_colors_with_shades, endpoint=False) + + arr_by_shade_rows = linear_nums.reshape(num_shades, -1) + arr_by_shade_columns = arr_by_shade_rows.T + num_partitions = arr_by_shade_columns.shape[0] + nums_distributed_like_rising_saw = arr_by_shade_columns.flatten() + + initial_cm = hsv(nums_distributed_like_rising_saw) + lower_partitions_half = num_partitions // 2 + upper_partitions_half = num_partitions - lower_partitions_half + + lower_half = lower_partitions_half * num_shades + initial_cm[:lower_half, :3] *= np.linspace(0.2, 1, lower_half)[:, np.newaxis] + + upper_half_indices = np.arange(lower_half, num_colors_with_shades).reshape( + upper_partitions_half, num_shades + ) + modifier = ( + (1 - initial_cm[upper_half_indices, :3]) + * np.arange(upper_partitions_half)[:, np.newaxis, np.newaxis] + / upper_partitions_half + ) + initial_cm[upper_half_indices, :3] += modifier + colors = initial_cm[:num_colors] + return colors diff --git a/cleanlab/token_classification/filter.py b/cleanlab/token_classification/filter.py index fcaf25b2fe..698efdd4fc 100644 --- a/cleanlab/token_classification/filter.py +++ b/cleanlab/token_classification/filter.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -31,6 +31,7 @@ def find_label_issues( pred_probs: list, *, return_indices_ranked_by: str = "self_confidence", + **kwargs, ) -> List[Tuple[int, int]]: """Identifies tokens with label issues in a token classification dataset. @@ -60,6 +61,10 @@ def find_label_issues( See :py:func:`cleanlab.filter.find_label_issues ` documentation for more details on each label quality scoring method. + kwargs: + Additional keyword arguments to pass into :py:func:`filter.find_label_issues ` + which is internally applied at the token level. Can include values like `n_jobs` to control parallel processing, `frac_noise`, etc. + Returns ------- issues: @@ -87,7 +92,10 @@ def find_label_issues( pred_probs_flatten = np.array([pred for pred_prob in pred_probs for pred in pred_prob]) issues_main = find_label_issues_main( - labels_flatten, pred_probs_flatten, return_indices_ranked_by=return_indices_ranked_by + labels_flatten, + pred_probs_flatten, + return_indices_ranked_by=return_indices_ranked_by, + **kwargs, ) lengths = [len(label) for label in labels] diff --git a/cleanlab/token_classification/rank.py b/cleanlab/token_classification/rank.py index bd7a3f59d0..be41568aa2 100644 --- a/cleanlab/token_classification/rank.py +++ b/cleanlab/token_classification/rank.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -154,9 +154,15 @@ def issues_from_scores( Converts scores output by :py:func:`token_classification.rank.get_label_quality_scores ` to a list of issues of similar format as output by :py:func:`token_classification.filter.find_label_issues `. - Only considers as issues those tokens with label quality score lower than `threshold`. + Issues are sorted by label quality score, from most to least severe. - Issues are sorted by label quality score, from most severe to least. + Only considers as issues those tokens with label quality score lower than `threshold`, + so this parameter determines the number of issues that are returned. + This method is intended for converting the most severely mislabeled examples to a format compatible with + ``summary`` methods like :py:func:`token_classification.summary.display_issues `. + This method does not estimate the number of label errors since the `threshold` is arbitrary, + for that instead use :py:func:`token_classification.filter.find_label_issues `, + which estimates the label errors via Confident Learning rather than score thresholding. Parameters ---------- @@ -275,8 +281,10 @@ def _softmin_sentence_score( return np.array([np.mean(scores) for scores in token_scores]) def softmax(scores: np.ndarray) -> np.ndarray: - exp_scores = np.exp(scores / temperature) - return exp_scores / np.sum(exp_scores) + scores = scores / temperature + scores_max = np.amax(scores, axis=0, keepdims=True) + exp_scores_shifted = np.exp(scores - scores_max) + return exp_scores_shifted / np.sum(exp_scores_shifted, axis=0, keepdims=True) def fun(scores: np.ndarray) -> float: return np.dot(scores, softmax(1 - np.array(scores))) diff --git a/cleanlab/token_classification/summary.py b/cleanlab/token_classification/summary.py index dc93de720b..bf1fe45580 100644 --- a/cleanlab/token_classification/summary.py +++ b/cleanlab/token_classification/summary.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify diff --git a/cleanlab/version.py b/cleanlab/version.py index 7c36fadd30..86e18e5262 100644 --- a/cleanlab/version.py +++ b/cleanlab/version.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -15,9 +15,27 @@ # along with cleanlab. If not, see . -__version__ = "2.2.1" +__version__ = "2.4.1" -# 2.2.1 - Not yet released, you are using developer version. See documentation at: https://docs.cleanlab.ai/master/ +# 2.4.1 - Not yet released, you are using bleeding-edge developer version. See its documentation at: https://docs.cleanlab.ai/master/ + +# ------------------------------------------------ +# | PREVIOUS MAJOR VERSION RELEASE NOTES SUMMARY | +# ------------------------------------------------ + +# 2.4.0 - One line of code to detect all sorts of dataset issues +# +# Major new functionalities include: +# - Datalab: A unified audit to detect different types of issues in your data and labels. This is the primary way most users should apply cleanlab to their dataset. +# - Nicer APIs for label issues in multi-label classification datasets, including dataset-level issue summaries for multi-label classification. +# - Updated tutorials with more interesting datasets and ML models. + +# 2.3.0 - Extending cleanlab beyond label errors into a complete library for data-centric AI +# +# Major new functionalities include: +# - Active learning with data re-labeling (ActiveLab) +# - KerasWrapperModel and KerasSequentialWrapper to make arbitrary Keras models compatible with scikit-learn +# - Computational improvements for detecting label issues (better efficiency and mini-batch estimation that works with lower memory) # 2.2.0 - Re-invented algorithms for multi-label classification and support for datasets with missing classes # @@ -30,12 +48,6 @@ # - cleanlab now works much better for datasets in which some classes happen to not be present. # - Algorithmic improvements to ensure count.num_label_issues() returns more accurate estimates. # - For developers: introduction of flake8 code linter and more comprehensive mypy type annotations. -# -# See release for a full changelog. - -# ------------------------------------------ -# | PREVIOUS VERSION RELEASE NOTES SUMMARY | -# ------------------------------------------ # 2.1.0 - "Multiannotator, Outlier detection, and Token Classification" - Cleanlab supports several new features # diff --git a/docs/requirements.txt b/docs/requirements.txt index 00db3f8b9e..2772a81f02 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -8,21 +8,20 @@ ipython==8.0.1 ipykernel==6.8.0 ipywidgets==7.6.5 sphinx-multiversion==0.2.4 -torchvision==0.12.0 sphinx-copybutton==0.5.0 sphinxcontrib-katex==0.8.6 -matplotlib==3.5.1 -skorch==0.11.0 -tensorflow-datasets==4.5.2 +sphinx-autodoc-typehints==1.19.2 +matplotlib==3.6.3 +requests==2.28.2 tensorflow==2.9.1 -scikeras==0.9.0 -scikit-learn<1.2.0 -speechbrain==0.5.12 tensorflow-io==0.26.0 -huggingface_hub==0.7 -torchaudio==0.11.0 -fasttext==0.9.2 -timm==0.6.5 -torch==1.11.0 -requests==2.28.0 -sphinx-autodoc-typehints==1.19.2 +sentence-transformers==2.2.2 +speechbrain==0.5.13 +huggingface_hub==0.11.1 +fasttext-wheel==0.9.2 +torch==1.13.1 +skorch==0.12.1 +torchvision==0.14.1 +torchaudio==0.13.1 +timm==0.6.12 +datasets>=2.9.0 diff --git a/docs/source/_static/css/custom.css b/docs/source/_static/css/custom.css index a3f141448c..365c0171b1 100644 --- a/docs/source/_static/css/custom.css +++ b/docs/source/_static/css/custom.css @@ -38,4 +38,4 @@ h5 { h6 { font-size: .75em; -} \ No newline at end of file +} diff --git a/docs/source/cleanlab/datalab/data.rst b/docs/source/cleanlab/datalab/data.rst new file mode 100644 index 0000000000..6da27efbc7 --- /dev/null +++ b/docs/source/cleanlab/datalab/data.rst @@ -0,0 +1,9 @@ +data +==== + +.. automodule:: cleanlab.datalab.data + :autosummary: + :members: + :undoc-members: + :show-inheritance: + :ignore-module-all: \ No newline at end of file diff --git a/docs/source/cleanlab/datalab/data_issues.rst b/docs/source/cleanlab/datalab/data_issues.rst new file mode 100644 index 0000000000..d3a578ae2b --- /dev/null +++ b/docs/source/cleanlab/datalab/data_issues.rst @@ -0,0 +1,9 @@ +data_issues +=========== + +.. automodule:: cleanlab.datalab.data_issues + :autosummary: + :members: + :undoc-members: + :show-inheritance: + :ignore-module-all: \ No newline at end of file diff --git a/docs/source/cleanlab/datalab/datalab.rst b/docs/source/cleanlab/datalab/datalab.rst new file mode 100644 index 0000000000..8a38a27f95 --- /dev/null +++ b/docs/source/cleanlab/datalab/datalab.rst @@ -0,0 +1,9 @@ +datalab +======= + +.. automodule:: cleanlab.datalab.datalab + :autosummary: + :members: + :undoc-members: + :show-inheritance: + :ignore-module-all: \ No newline at end of file diff --git a/docs/source/cleanlab/datalab/factory.rst b/docs/source/cleanlab/datalab/factory.rst new file mode 100644 index 0000000000..d65d1eac5d --- /dev/null +++ b/docs/source/cleanlab/datalab/factory.rst @@ -0,0 +1,9 @@ +factory +======= + +.. automodule:: cleanlab.datalab.factory + :autosummary: + :members: + :undoc-members: + :show-inheritance: + :ignore-module-all: \ No newline at end of file diff --git a/docs/source/cleanlab/datalab/guide/custom_issue_manager.rst b/docs/source/cleanlab/datalab/guide/custom_issue_manager.rst new file mode 100644 index 0000000000..3ff417ec86 --- /dev/null +++ b/docs/source/cleanlab/datalab/guide/custom_issue_manager.rst @@ -0,0 +1,227 @@ +.. _issue_manager_creating_your_own: + +Creating Your Own Issues Manager +================================ + + + +This guide walks through the process of creating creating your own +:py:class:`IssueManager ` +to detect a custom-defined type of issue alongside the pre-defined issue types in +:py:class:`Datalab `. + +.. seealso:: + + - :py:meth:`register `: + You can either use this function at runtime to register a new issue manager: + + .. code-block:: python + + from cleanlab.datalab.factory import register + register(MyIssueManager) + + or add as a decorator to the class definition: + + .. code-block:: python + + @register + class MyIssueManager(IssueManager): + ... + +Prerequisites +------------- + +As a starting point for this guide, we'll import the necessary things for the next section and create a dummy dataset. + +.. note:: + + .. include:: ../optional_dependencies.rst + +.. code-block:: python + + + import numpy as np + import pandas as pd + from cleanlab import IssueManager + + # Create a dummy dataset + N = 20 + data = pd.DataFrame( + { + "text": [f"example {i}" for i in range(N)], + "label": np.random.randint(0, 2, N), + }, + ) + + +Implementing IssueManagers +-------------------------- + +.. _basic_issue_manager: + +Basic Issue Check +~~~~~~~~~~~~~~~~~ + + +To create a basic issue manager, inherit from the +:py:class:`IssueManager ` class, +assign a name to the class as the class-variable, `issue_name`, and implement the +:py:meth:`find_issues ` method. + +The :py:meth:`find_issues ` +method should mark each example in the dataset as an issue or not with a boolean array. +It should also provide a score for each example in the dataset that quantifies the quality of the example +with regards to the issue. + +.. code-block:: python + + class Basic(IssueManager): + # Assign a name to the issue + issue_name = "basic" + def find_issues(self, **kwargs) -> None: + # Compute scores for each example + scores = np.random.rand(len(self.datalab.data)) + + # Construct a dataframe where examples are marked for issues + # and the score for each example is included. + self.issues = pd.DataFrame( + { + f"is_{self.issue_name}_issue" : scores < 0.1, + self.issue_score_key : scores, + }, + ) + + # Score the dataset as a whole based on this issue type + self.summary = self.make_summary(score = scores.mean()) + + +.. _intermediate_issue_manager: + +Intermediate Issue Check +~~~~~~~~~~~~~~~~~~~~~~~~ + + +To create an intermediate issue: + +- Perform the same steps as in the :ref:`basic issue check ` section. +- Populate the `info` attribute with a dictionary of information about the identified issues. + +The information can be included in a report generated by :py:class:`Datalab `, +if you add any of the keys to the `verbosity_levels` class-attribute. +Optionally, you can also add a description of the type of issue this issue manager handles to the `description` class-attribute. + +.. code-block:: python + + class Intermediate(IssueManager): + issue_name = "intermediate" + # Add a dictionary of information to include in the report + verbosity_levels = { + 0: [], + 1: ["std"], + 2: ["raw_scores"], + } + # Add a description of the issue + description = "Intermediate issues are a bit more involved than basic issues." + def find_issues(self, *, intermediate_arg: int, **kwargs) -> None: + N = len(self.datalab.data) + raw_scores = np.random.rand(N) + std = raw_scores.std() + threshold = min(0, raw_scores.mean() - std) + sin_filter = np.sin(intermediate_arg * np.arange(N) / N) + kernel = sin_filter ** 2 + scores = kernel * raw_scores + self.issues = pd.DataFrame( + { + f"is_{self.issue_name}_issue" : scores < threshold, + self.issue_score_key : scores, + }, + ) + self.summary = self.make_summary(score = scores.mean()) + + # Useful information that will be available in the Datalab instance + self.info = { + "std": std, + "raw_scores": raw_scores, + "kernel": kernel, + } + +Advanced Issue Check +~~~~~~~~~~~~~~~~~~~~ + +.. note:: + + WIP: This section is a work in progress. + + + +Use with Datalab +---------------- + +We can create a +:py:class:`Datalab ` +instance and run issue checks with the custom issue managers we created like so: + + +.. code-block:: python + + from cleanlab.datalab.factory import register + from cleanlab import Datalab + + + # Register the issue manager + for issue_manager in [Basic, Intermediate]: + register(issue_manager) + + # Instantiate a datalab instance + datalab = Datalab(data, label_name="label") + + # Run the issue check + issue_types = {"basic": {}, "intermediate": {"intermediate_arg": 2}} + datalab.find_issues(issue_types=issue_types) + + # Print report + datalab.report(verbosity=0) + + +The report will look something like this: + +.. code-block:: text + + Here is a summary of the different kinds of issues found in the data: + + issue_type score num_issues + basic 0.477762 2 + intermediate 0.286455 0 + + (Note: A lower score indicates a more severe issue across all examples in the dataset.) + + + ------------------------------------------- basic issues ------------------------------------------- + + Number of examples with this issue: 2 + Overall dataset quality in terms of this issue: 0.4778 + + Examples representing most severe instances of this issue: + is_basic_issue basic_score + 13 True 0.003042 + 8 True 0.058117 + 11 False 0.121908 + 15 False 0.169312 + 17 False 0.229044 + + + --------------------------------------- intermediate issues ---------------------------------------- + + About this issue: + Intermediate issues are a bit more involved than basic issues. + + Number of examples with this issue: 0 + Overall dataset quality in terms of this issue: 0.2865 + + Examples representing most severe instances of this issue: + is_intermediate_issue intermediate_score kernel + 0 False 0.000000 0.0 + 1 False 0.007059 0.009967 + 3 False 0.010995 0.087332 + 2 False 0.016296 0.03947 + 11 False 0.019459 0.794251 diff --git a/docs/source/cleanlab/datalab/guide/index.rst b/docs/source/cleanlab/datalab/guide/index.rst new file mode 100644 index 0000000000..b3dd678816 --- /dev/null +++ b/docs/source/cleanlab/datalab/guide/index.rst @@ -0,0 +1,29 @@ +Datalab guides +============== + +This page contains a list of guides for using Datalab. + +.. note:: + + .. include:: ../optional_dependencies.rst + + +Types of issues +--------------- + +These guides are for users who want to use Datalab with greater control, selecting what issues to search for and what nondefault settings to use for detecting them. + +.. toctree:: + :maxdepth: 3 + + issue_type_description + +Customizing issue types +----------------------- + +These guides are for developers to create a custom issue type that Datalab can audit for together with the built-in issue types it already detects. + +.. toctree:: + :maxdepth: 3 + + custom_issue_manager \ No newline at end of file diff --git a/docs/source/cleanlab/datalab/guide/issue_type_description.rst b/docs/source/cleanlab/datalab/guide/issue_type_description.rst new file mode 100644 index 0000000000..34b411341d --- /dev/null +++ b/docs/source/cleanlab/datalab/guide/issue_type_description.rst @@ -0,0 +1,211 @@ +Datalab Issue Types +******************* + + +Types of issues Datalab can detect +=================================== + +This page describes the various types of issues that Datalab can detect in a dataset. +For each type of issue, we explain: what it says about your data if detected, why this matters, and what parameters you can optionally specify to control the detection of this issue. + +Estimates for Each Issue Type +------------------------------ + +Datalab produces three estimates for **each** type of issue (called say `` here): + + +1. A numeric quality score `_score` (between 0 and 1) estimating how severe this issue is exhibited in each example from a dataset. Examples with higher scores are less likely to suffer from this issue. Access these via: the :py:attr:`Datalab.issues ` attribute or the method :py:meth:`Datalab.get_issues(\) `. +2. A Boolean `is__issue` flag for each example from a dataset. Examples where this has value `True` are those estimated to exhibit this issue. Access these via: the :py:attr:`Datalab.issues ` attribute or the method :py:meth:`Datalab.get_issues(\) `. +3. An overall dataset quality score (between 0 and 1), quantifying how severe this issue is overall across the entire dataset. Datasets with higher scores do not exhibit this issue as badly overall. Access these via: the :py:attr:`Datalab.issue_summary ` attribute. + +**Example (for the outlier issue type)** + +.. code-block:: python + + issue_name = "outlier" # how to reference the outlier issue type in code + issue_score = "outlier_score" # name of column with quality scores for the outlier issue type, atypical datapoints receive lower scores + is_issue = "is_outlier_issue" # name of Boolean column flagging which datapoints are considered outliers in the dataset + +Datalab estimates various issues based on the four inputs below. +Each input is optional, if you do not provide it, Datalab will skip checks for those types of issues that require this input. + +1. ``label_name`` - a field in the dataset that the stores the annotated class for each example in a multi-class classification dataset. +2. ``pred_probs`` - predicted class probabilities output by your trained model for each example in the dataset (these should be out-of-sample, eg. produced via cross-validation). +3. ``features`` - numeric vector representations of the features for each example in the dataset. These may be embeddings from a (pre)trained model, or just a numerically-transformed version of the original data features. +4. ``knn_graph`` - K nearest neighbor graph represented as a sparse matrix of dissimilarity values between examples in the dataset. If both `knn_graph` and `features` are provided, the `knn_graph` takes precedence, and if only `features` is provided, then a `knn_graph` is internally constructed based on the (either euclidean or cosine) distance between different examples’ features. + +Label Issue +----------- + +Examples whose given label is estimated to be potentially incorrect (e.g. due to annotation error) are flagged as having label issues. +Datalab estimates which examples appear mislabeled as well as a numeric label quality score for each, which quantifies the likelihood that an example is correctly labeled. + +For now, Datalab can only detect label issues in a multi-class classification dataset. +The cleanlab library has alternative methods you can us to detect label issues in other types of datasets (multi-label, multi-annotator, token classification, etc.). + +Label issues are calculated based on provided `pred_probs` from a trained model. If you do not provide this argument, this type of issue will not be considered. +For the most accurate results, provide out-of-sample `pred_probs` which can be obtained for a dataset via `cross-validation `_. + +Having mislabeled examples in your dataset may hamper the performance of supervised learning models you train on this data. +For evaluating models or performing other types of data analytics, mislabeled examples may lead you to draw incorrect conclusions. +To handle mislabeled examples, you can either filter out the data with label issues or try to correct their labels. + + + +Outlier Issue +------------- + +Examples that are very different from the rest of the dataset (i.e. potentially out-of-distribution or rare/anomalous instances). + +Outlier issues are calculated based on provided `features` , `knn_graph` , or `pred_probs`. +If you do not provide one of these arguments, this type of issue will not be considered. +This article describes how outlier issues are detected in a dataset: `https://cleanlab.ai/blog/outlier-detection/ `_. + +When based on `features` or `knn_graph`, the outlier quality of each example is scored inversely proportional to its distance to its K nearest neighbors in the dataset. + +When based on `pred_probs`, the outlier quality of each example is scored inversely proportional to the uncertainty in its prediction. + +Modeling data with outliers may have unexpected consequences. +Closely inspect them and consider removing some outliers that may be negatively affecting your models. + +(Near) Duplicate Issue +---------------------- + +A (near) duplicate issue refers to two or more examples in a dataset that are extremely similar to each other, relative to the rest of the dataset. +The examples flagged with this issue may be exactly duplicated, or lie atypically close together when represented as vectors (i.e. feature embeddings). +Near duplicated examples may record the same information with different: + +- Abbreviations, misspellings, typos, formatting, etc. in text data. +- Compression formats, resolutions, or sampling rates in image, video, and audio data. +- Minor variations which naturally occur in many types of data (e.g. translated versions of an image). + +Near Duplicate issues are calculated based on provided `features` or `knn_graph`. +If you do not provide one of these arguments, this type of issue will not be considered. + +Datalab defines near duplicates as those examples whose distance to their nearest neighbor (in the space of provided `features`) in the dataset is less than `c * D`, where `0 < c < 1` is a fractional constant parameter, and `D` is the median (over the full dataset) of such distances between each example and its nearest neighbor. +Scoring the numeric quality of an example in terms of the near duplicate issue type is done proportionally to its distance to its nearest neighbor. + +Including near-duplicate examples in a dataset may negatively impact a ML model's generalization performance and lead to overfitting. +In particular, it is questionable to include examples in a test dataset which are (nearly) duplicated in the corresponding training dataset. +More generally, examples which happen to be duplicated can affect the final modeling results much more than other examples — so you should at least be aware of their presence. + +Non-IID Issue +------------- + +Whether the dataset exhibits statistically significant violations of the IID assumption like: changepoints or shift, drift, autocorrelation, etc. The specific form of violation considered is whether the examples are ordered such that almost adjacent examples tend to have more similar feature values. If you care about this check, do **not** first shuffle your dataset -- this check is entirely based on the sequential order of your data. + +The Non-IID issue is detected based on provided `features` or `knn_graph`. If you do not provide one of these arguments, this type of issue will not be considered. + +Mathematically, the **overall** Non-IID score for the dataset is defined as the p-value of a statistical test for whether the distribution of *index-gap* values differs between group A vs. group B defined as follows. For a pair of examples in the dataset `x1, x2`, we define their *index-gap* as the distance between the indices of these examples in the ordering of the data (e.g. if `x1` is the 10th example and `x2` is the 100th example in the dataset, their index-gap is 90). We construct group A from pairs of examples which are amongst the K nearest neighbors of each other, where neighbors are defined based on the provided `knn_graph` or via distances in the space of the provided vector `features` . Group B is constructed from random pairs of examples in the dataset. + +The Non-IID quality score for each example `x` is defined via a similarly computed p-value but with Group A constructed from the K nearest neighbors of `x` and Group B constructed from random examples from the dataset paired with `x`. + +The assumption that examples in a dataset are Independent and Identically Distributed (IID) is fundamental to most proper modeling. Detecting all possible violations of the IID assumption is statistically impossible. This issue type only considers specific forms of violation where examples that tend to be closer together in the dataset ordering also tend to have more similar feature values. This includes scenarios where: + +- The underlying distribution from which examples stem is evolving over time (not identically distributed). +- An example can influence the values of future examples in the dataset (not independent). + +For datasets with low non-IID score, you should consider why your data are not IID and act accordingly. For example, if the data distribution is drifting over time, consider employing a time-based train/test split instead of a random partition. Note that shuffling the data ahead of time will ensure a good non-IID score, but this is not always a fix to the underlying problem (e.g. future deployment data may stem from a different distribution, or you may overlook the fact that examples influence each other). We thus recommend **not** shuffling your data to be able to diagnose this issue if it exists. + +Optional Issue Parameters +========================= + +Here is the dict of possible (**optional**) parameter values that can be specified via the argument `issue_types` to :py:meth:`Datalab.find_issues `. +Optionally specify these to exert greater control over how issues are detected in your dataset. +Appropriate defaults are used for any parameters you do not specify, so no need to specify all of these! + +.. code-block:: python + + possible_issue_types = { + "label": label_kwargs, "outlier": outlier_kwargs, + "near_duplicate": near_duplicate_kwargs, "non_iid": non_iid_kwargs + } + + +where the possible `kwargs` dicts for each key are described in the sections below. + +Label Issue Parameters +---------------------- + +.. code-block:: python + + label_kwargs = { + "health_summary_parameters": # dict of potential keyword arguments to method `dataset.health_summary()`, + "clean_learning_kwargs": # dict of keyword arguments to constructor `CleanLearning()` including keys like: "find_label_issues_kwargs" or "label_quality_scores_kwargs", + "thresholds": # `thresholds` argument to `CleanLearning.find_label_issues()`, + "noise_matrix": # `noise_matrix` argument to `CleanLearning.find_label_issues()`, + "inverse_noise_matrix": # `inverse_noise_matrix` argument to `CleanLearning.find_label_issues()`, + "save_space": # `save_space` argument to `CleanLearning.find_label_issues()`, + "clf_kwargs": # `clf_kwargs` argument to `CleanLearning.find_label_issues()`. Currently has no effect., + "validation_func": # `validation_func` argument to `CleanLearning.fit()`. Currently has no effect., + } + +.. attention:: + + ``health_summary_parameters`` and ``health_summary_kwargs`` can work in tandem to determine the arguments to be used in the call to :py:meth:`dataset.health_summary `. + +.. note:: + + For more information, view the source code of: :py:class:`datalab.issue_manager.label.LabelIssueManager `. + +Outlier Issue Parameters +------------------------ + +.. code-block:: python + + outlier_kwargs = { + "threshold": # floating value between 0 and 1 that sets the sensitivity of the outlier detection algorithms, based on either features or pred_probs.. + "ood_kwargs": # dict of keyword arguments to constructor `OutOfDistribution()`{ + "params": { + # NOTE: Each of the following keyword arguments can also be provided outside "ood_kwargs" + + "knn": # `knn` argument to constructor `OutOfDistribution()`. Used with features, + "k": # `k` argument to constructor `OutOfDistribution()`. Used with features, + "t": # `t` argument to constructor `OutOfDistribution()`. Used with features, + "adjust_pred_probs": # `adjust_pred_probs` argument to constructor `OutOfDistribution()`. Used with pred_probs, + "method": # `method` argument to constructor `OutOfDistribution()`. Used with pred_probs, + "confident_thresholds": # `confident_thresholds` argument to constructor `OutOfDistribution()`. Used with pred_probs, + }, + }, + } + +.. note:: + + For more information, view the source code of: :py:class:`datalab.issue_manager.outlier.OutlierIssueManager `. + +Duplicate Issue Parameters +-------------------------- + +.. code-block:: python + + near_duplicate_kwargs = { + "metric": # string representing the distance metric used in nearest neighbors search (passed as argument to `NearestNeighbors`), if necessary, + "k": # integer representing the number of nearest neighbors for nearest neighbors search (passed as argument to `NearestNeighbors`), if necessary, + "threshold": # `threshold` argument to constructor of `NearDuplicateIssueManager()`. Non-negative floating value that determines the maximum distance between two examples to be considered outliers, relative to the median distance to the nearest neighbors, + } + +.. attention:: + + `k` does not affect the results of the (near) duplicate search algorithm. It only affects the construction of the knn graph, if necessary. + +.. note:: + + For more information, view the source code of: :py:class:`datalab.issue_manager.duplicate.NearDuplicateIssueManager `. + + +Non-IID Issue Parameters +------------------------ + +.. code-block:: python + + non_iid_kwargs = { + "metric": # `metric` argument to constructor of `NonIIDIssueManager`. String for the distance metric used for nearest neighbors search if necessary. `metric` argument to constructor of `sklearn.neighbors.NearestNeighbors`, + "k": # `k` argument to constructor of `NonIIDIssueManager`. Integer representing the number of nearest neighbors for nearest neighbors search if necessary. `n_neighbors` argument to constructor of `sklearn.neighbors.NearestNeighbors`, + "num_permutations": # `num_permutations` argument to constructor of `NonIIDIssueManager`, + "seed": # seed for numpy's random number generator (used for permutation tests), + "significance_threshold": # `significance_threshold` argument to constructor of `NonIIDIssueManager`. Floating value between 0 and 1 that determines the overall signicance of non-IID issues found in the dataset. + } + +.. note:: + + For more information, view the source code of: :py:class:`datalab.issue_manager.noniid.NonIIDIssueManager `. diff --git a/docs/source/cleanlab/datalab/index.rst b/docs/source/cleanlab/datalab/index.rst new file mode 100644 index 0000000000..9f1b04dfba --- /dev/null +++ b/docs/source/cleanlab/datalab/index.rst @@ -0,0 +1,39 @@ +datalab +======= + +.. warning:: + Methods in this ``datalab`` module are bleeding edge and may have sharp edges. They are not guaranteed to be stable between different ``cleanlab`` versions. + +.. automodule:: cleanlab.datalab + :autosummary: + :members: + :undoc-members: + :show-inheritance: + +Getting Started +--------------- + +.. include:: optional_dependencies.rst + +Guides +------ + +.. toctree:: + :maxdepth: 2 + + guide/index + + +API Reference +------------- + +.. toctree:: + :maxdepth: 2 + + datalab + data + data_issues + issue_finder + factory + issue_manager/index + report diff --git a/docs/source/cleanlab/datalab/issue_finder.rst b/docs/source/cleanlab/datalab/issue_finder.rst new file mode 100644 index 0000000000..d5540dd90b --- /dev/null +++ b/docs/source/cleanlab/datalab/issue_finder.rst @@ -0,0 +1,12 @@ +issue_finder +============ + +.. note:: This module is not intended to be used directly by users. It is used by the :mod:`cleanlab.datalab.datalab` module. + Specifically, it is used by the :py:meth:`Datalab.find_issues ` method. + +.. automodule:: cleanlab.datalab.issue_finder + :autosummary: + :members: + :undoc-members: + :show-inheritance: + :ignore-module-all: \ No newline at end of file diff --git a/docs/source/cleanlab/datalab/issue_manager/duplicate.rst b/docs/source/cleanlab/datalab/issue_manager/duplicate.rst new file mode 100644 index 0000000000..37d6bc8c9b --- /dev/null +++ b/docs/source/cleanlab/datalab/issue_manager/duplicate.rst @@ -0,0 +1,9 @@ +duplicate +========= + + +.. automodule:: cleanlab.datalab.issue_manager.duplicate + :autosummary: + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/cleanlab/datalab/issue_manager/index.rst b/docs/source/cleanlab/datalab/issue_manager/index.rst new file mode 100644 index 0000000000..92815b8c9c --- /dev/null +++ b/docs/source/cleanlab/datalab/issue_manager/index.rst @@ -0,0 +1,13 @@ +issue_manager +============= + +.. warning:: + Methods in this ``issue_manager`` module are bleeding edge and may have sharp edges. They are not guaranteed to be stable between different ``cleanlab`` versions. + + +.. toctree:: + Base issue_manager module + label + outlier + duplicate + noniid \ No newline at end of file diff --git a/docs/source/cleanlab/datalab/issue_manager/issue_manager.rst b/docs/source/cleanlab/datalab/issue_manager/issue_manager.rst new file mode 100644 index 0000000000..a0974cf86a --- /dev/null +++ b/docs/source/cleanlab/datalab/issue_manager/issue_manager.rst @@ -0,0 +1,8 @@ +issue_manager +============= + +.. automodule:: cleanlab.datalab.issue_manager.issue_manager + :autosummary: + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/cleanlab/datalab/issue_manager/label.rst b/docs/source/cleanlab/datalab/issue_manager/label.rst new file mode 100644 index 0000000000..334bce732f --- /dev/null +++ b/docs/source/cleanlab/datalab/issue_manager/label.rst @@ -0,0 +1,8 @@ +label +===== + +.. automodule:: cleanlab.datalab.issue_manager.label + :autosummary: + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/cleanlab/datalab/issue_manager/noniid.rst b/docs/source/cleanlab/datalab/issue_manager/noniid.rst new file mode 100644 index 0000000000..c93df82679 --- /dev/null +++ b/docs/source/cleanlab/datalab/issue_manager/noniid.rst @@ -0,0 +1,9 @@ +noniid +======= + + +.. automodule:: cleanlab.datalab.issue_manager.noniid + :autosummary: + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/cleanlab/datalab/issue_manager/outlier.rst b/docs/source/cleanlab/datalab/issue_manager/outlier.rst new file mode 100644 index 0000000000..c2ff3ab62c --- /dev/null +++ b/docs/source/cleanlab/datalab/issue_manager/outlier.rst @@ -0,0 +1,9 @@ +outlier +======= + + +.. automodule:: cleanlab.datalab.issue_manager.outlier + :autosummary: + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/cleanlab/datalab/optional_dependencies.rst b/docs/source/cleanlab/datalab/optional_dependencies.rst new file mode 100644 index 0000000000..9fd6fc73c4 --- /dev/null +++ b/docs/source/cleanlab/datalab/optional_dependencies.rst @@ -0,0 +1,11 @@ +This package has additional dependencies that are not required for the core ``cleanlab`` package. To install them, run: + +.. code-block:: console + + $ pip install "cleanlab[datalab]" + +For the developmental version of the package, install from source: + +.. code-block:: console + + $ pip install "git+https://github.com/cleanlab/cleanlab.git#egg=cleanlab[datalab]" \ No newline at end of file diff --git a/docs/source/cleanlab/datalab/report.rst b/docs/source/cleanlab/datalab/report.rst new file mode 100644 index 0000000000..3f5f84d4d6 --- /dev/null +++ b/docs/source/cleanlab/datalab/report.rst @@ -0,0 +1,12 @@ +report +====== + +.. note:: This module is not intended to be used directly by users. It is used by the :mod:`cleanlab.datalab.datalab` module. + Specifically, it is used by the :py:meth:`Datalab.report ` method. + +.. automodule:: cleanlab.datalab.report + :autosummary: + :members: + :undoc-members: + :show-inheritance: + :ignore-module-all: \ No newline at end of file diff --git a/docs/source/cleanlab/dataset.rst b/docs/source/cleanlab/dataset.rst index 0b4f7526cb..1433f8d030 100644 --- a/docs/source/cleanlab/dataset.rst +++ b/docs/source/cleanlab/dataset.rst @@ -6,3 +6,67 @@ dataset :members: :undoc-members: :show-inheritance: + +.. testsetup:: * + + import cleanlab + import numpy as np + from cleanlab.benchmarking import noise_generation + + SEED = 0 + + def get_data_labels_from_dataset( + means=[[3, 2], [7, 7], [0, 8], [0, 10]], + covs=[ + [[5, -1.5], [-1.5, 1]], + [[1, 0.5], [0.5, 4]], + [[5, 1], [1, 5]], + [[3, 1], [1, 1]], + ], + sizes=[100, 50, 50, 50], + avg_trace=0.8, + seed=SEED, # set to None for non-reproducible randomness + ): + np.random.seed(seed=SEED) + + K = len(means) # number of classes + data = [] + labels = [] + test_data = [] + test_labels = [] + + for idx in range(K): + data.append( + np.random.multivariate_normal( + mean=means[idx], cov=covs[idx], size=sizes[idx] + ) + ) + test_data.append( + np.random.multivariate_normal( + mean=means[idx], cov=covs[idx], size=sizes[idx] + ) + ) + labels.append(np.array([idx for i in range(sizes[idx])])) + test_labels.append(np.array([idx for i in range(sizes[idx])])) + X_train = np.vstack(data) + y_train = np.hstack(labels) + X_test = np.vstack(test_data) + y_test = np.hstack(test_labels) + + # Compute p(y=k) the prior distribution over true labels. + py_true = np.bincount(y_train) / float(len(y_train)) + + noise_matrix_true = noise_generation.generate_noise_matrix_from_trace( + K, + trace=avg_trace * K, + py=py_true, + valid_noise_matrix=True, + seed=SEED, + ) + + # Generate our noisy labels using the noise_marix. + s = noise_generation.generate_noisy_labels(y_train, noise_matrix_true) + s_test = noise_generation.generate_noisy_labels(y_test, noise_matrix_true) + ps = np.bincount(s) / float(len(s)) # Prior distribution over noisy labels + + return X_train, s \ No newline at end of file diff --git a/docs/source/cleanlab/experimental/index.rst b/docs/source/cleanlab/experimental/index.rst index 4d32910206..798be77934 100644 --- a/docs/source/cleanlab/experimental/index.rst +++ b/docs/source/cleanlab/experimental/index.rst @@ -4,6 +4,10 @@ experimental .. warning:: Methods in this ``experimental`` module are bleeding edge and may have sharp edges. They are not guaranteed to be stable between different ``cleanlab`` versions. +Useful methods/models adapted for use with cleanlab. + +Some of these files include various models that can be used with cleanlab to find issues in specific types of data. These require dependencies on deep learning and other machine learning packages that are not official cleanlab dependencies. You must install these dependencies on your own if you wish to use them. + .. automodule:: cleanlab.experimental :autosummary: :members: @@ -11,8 +15,7 @@ experimental :show-inheritance: .. toctree:: - keras - fasttext + label_issues_batched mnist_pytorch coteaching cifar_cnn diff --git a/docs/source/cleanlab/experimental/label_issues_batched.rst b/docs/source/cleanlab/experimental/label_issues_batched.rst new file mode 100644 index 0000000000..1262958fed --- /dev/null +++ b/docs/source/cleanlab/experimental/label_issues_batched.rst @@ -0,0 +1,8 @@ +label_issues_batched +==================== + +.. automodule:: cleanlab.experimental.label_issues_batched + :autosummary: + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/cleanlab/internal/index.rst b/docs/source/cleanlab/internal/index.rst index 0601499cb4..6642479281 100644 --- a/docs/source/cleanlab/internal/index.rst +++ b/docs/source/cleanlab/internal/index.rst @@ -17,5 +17,6 @@ internal label_quality_utils multilabel_utils multilabel_scorer + outlier token_classification_utils validation diff --git a/docs/source/cleanlab/internal/outlier.rst b/docs/source/cleanlab/internal/outlier.rst new file mode 100644 index 0000000000..26c516758b --- /dev/null +++ b/docs/source/cleanlab/internal/outlier.rst @@ -0,0 +1,8 @@ +outlier +======= + +.. automodule:: cleanlab.internal.outlier + :autosummary: + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/cleanlab/experimental/fasttext.rst b/docs/source/cleanlab/models/fasttext.rst similarity index 65% rename from docs/source/cleanlab/experimental/fasttext.rst rename to docs/source/cleanlab/models/fasttext.rst index 792b769342..78efe7677a 100644 --- a/docs/source/cleanlab/experimental/fasttext.rst +++ b/docs/source/cleanlab/models/fasttext.rst @@ -1,7 +1,7 @@ fasttext ======== -.. automodule:: cleanlab.experimental.fasttext +.. automodule:: cleanlab.models.fasttext :autosummary: :members: :undoc-members: diff --git a/docs/source/cleanlab/models/index.rst b/docs/source/cleanlab/models/index.rst new file mode 100644 index 0000000000..8c34d0e267 --- /dev/null +++ b/docs/source/cleanlab/models/index.rst @@ -0,0 +1,20 @@ +models +====== + +.. warning:: + Methods in this ``models`` module are not guaranteed to be stable between different ``cleanlab`` versions. + +Useful models adapted for use with cleanlab. + +Some of these files include various models that can be used with cleanlab to find issues in specific types of data. These require dependencies on deep learning and other machine learning packages that are not official cleanlab dependencies. You must install these dependencies on your own if you wish to use them. + + +.. automodule:: cleanlab.models + :autosummary: + :members: + :undoc-members: + :show-inheritance: + +.. toctree:: + keras + fasttext \ No newline at end of file diff --git a/docs/source/cleanlab/experimental/keras.rst b/docs/source/cleanlab/models/keras.rst similarity index 58% rename from docs/source/cleanlab/experimental/keras.rst rename to docs/source/cleanlab/models/keras.rst index f35f93c455..c9ff1b3138 100644 --- a/docs/source/cleanlab/experimental/keras.rst +++ b/docs/source/cleanlab/models/keras.rst @@ -1,7 +1,7 @@ keras ===== -.. automodule:: cleanlab.experimental.keras +.. automodule:: cleanlab.models.keras :autosummary: :members: :undoc-members: diff --git a/docs/source/cleanlab/multilabel_classification.rst b/docs/source/cleanlab/multilabel_classification.rst deleted file mode 100644 index 02c35acebc..0000000000 --- a/docs/source/cleanlab/multilabel_classification.rst +++ /dev/null @@ -1,8 +0,0 @@ -multilabel_classification -========================= - -.. automodule:: cleanlab.multilabel_classification - :autosummary: - :members: - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/docs/source/cleanlab/multilabel_classification/dataset.rst b/docs/source/cleanlab/multilabel_classification/dataset.rst new file mode 100644 index 0000000000..b1c2544548 --- /dev/null +++ b/docs/source/cleanlab/multilabel_classification/dataset.rst @@ -0,0 +1,8 @@ +dataset +======= + +.. automodule:: cleanlab.multilabel_classification.dataset + :autosummary: + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/cleanlab/multilabel_classification/filter.rst b/docs/source/cleanlab/multilabel_classification/filter.rst new file mode 100644 index 0000000000..2991848414 --- /dev/null +++ b/docs/source/cleanlab/multilabel_classification/filter.rst @@ -0,0 +1,8 @@ +filter +====== + +.. automodule:: cleanlab.multilabel_classification.filter + :autosummary: + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/cleanlab/multilabel_classification/index.rst b/docs/source/cleanlab/multilabel_classification/index.rst new file mode 100644 index 0000000000..a73dcd25a1 --- /dev/null +++ b/docs/source/cleanlab/multilabel_classification/index.rst @@ -0,0 +1,22 @@ +multilabel_classification +========================= + +Methods to detect data and label issues in multi-label classification datasets. + +In multi-class classification, each example in the dataset belongs to exactly 1 out of K classes (e.g. if classifying animals as: {dog, cat, rat}). + +In multi-label classification, each example in the dataset can belong to 1 or more classes (out of K possible classes), or none of the classes at all (e.g. if classifying animals as: {predator, pet, reptile}). + + + +.. automodule:: cleanlab.multilabel_classification + :autosummary: + :members: + :undoc-members: + :show-inheritance: + +.. toctree:: + + filter + rank + dataset \ No newline at end of file diff --git a/docs/source/cleanlab/multilabel_classification/rank.rst b/docs/source/cleanlab/multilabel_classification/rank.rst new file mode 100644 index 0000000000..4c7b2c35be --- /dev/null +++ b/docs/source/cleanlab/multilabel_classification/rank.rst @@ -0,0 +1,8 @@ +rank +==== + +.. automodule:: cleanlab.multilabel_classification.rank + :autosummary: + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/cleanlab/object_detection/filter.rst b/docs/source/cleanlab/object_detection/filter.rst new file mode 100644 index 0000000000..81f60befd3 --- /dev/null +++ b/docs/source/cleanlab/object_detection/filter.rst @@ -0,0 +1,9 @@ +filter +==== + +.. automodule:: cleanlab.object_detection.filter + :autosummary: + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/source/cleanlab/object_detection/index.rst b/docs/source/cleanlab/object_detection/index.rst new file mode 100644 index 0000000000..5d80b4f744 --- /dev/null +++ b/docs/source/cleanlab/object_detection/index.rst @@ -0,0 +1,16 @@ +object_detection +==================== + +Methods to detect label issues in object detection datasets. + +.. automodule:: cleanlab.object_detection + :autosummary: + :members: + :undoc-members: + :show-inheritance: + +.. toctree:: + + rank + filter + summary diff --git a/docs/source/cleanlab/object_detection/rank.rst b/docs/source/cleanlab/object_detection/rank.rst new file mode 100644 index 0000000000..45b935e484 --- /dev/null +++ b/docs/source/cleanlab/object_detection/rank.rst @@ -0,0 +1,9 @@ +rank +==== + +.. automodule:: cleanlab.object_detection.rank + :autosummary: + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/source/cleanlab/object_detection/summary.rst b/docs/source/cleanlab/object_detection/summary.rst new file mode 100644 index 0000000000..8de4222d1d --- /dev/null +++ b/docs/source/cleanlab/object_detection/summary.rst @@ -0,0 +1,9 @@ +summary +==== + +.. automodule:: cleanlab.object_detection.summary + :autosummary: + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/source/cleanlab/regression/index.rst b/docs/source/cleanlab/regression/index.rst new file mode 100644 index 0000000000..37e7b2b95f --- /dev/null +++ b/docs/source/cleanlab/regression/index.rst @@ -0,0 +1,15 @@ +regression +==================== + +Methods to detect data and label issues in regression datasets. + +.. automodule:: cleanlab.regression + :autosummary: + :members: + :undoc-members: + :show-inheritance: + +.. toctree:: + + rank + learn \ No newline at end of file diff --git a/docs/source/cleanlab/regression/learn.rst b/docs/source/cleanlab/regression/learn.rst new file mode 100644 index 0000000000..94c20f163d --- /dev/null +++ b/docs/source/cleanlab/regression/learn.rst @@ -0,0 +1,8 @@ +regression.learn +================ + +.. automodule:: cleanlab.regression.learn + :autosummary: + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/cleanlab/regression/rank.rst b/docs/source/cleanlab/regression/rank.rst new file mode 100644 index 0000000000..24320ac2b9 --- /dev/null +++ b/docs/source/cleanlab/regression/rank.rst @@ -0,0 +1,8 @@ +regression.rank +=============== + +.. automodule:: cleanlab.regression.rank + :autosummary: + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/cleanlab/segmentation/filter.rst b/docs/source/cleanlab/segmentation/filter.rst new file mode 100644 index 0000000000..0b306746f4 --- /dev/null +++ b/docs/source/cleanlab/segmentation/filter.rst @@ -0,0 +1,8 @@ +filter +==== + +.. automodule:: cleanlab.segmentation.filter + :autosummary: + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/cleanlab/segmentation/index.rst b/docs/source/cleanlab/segmentation/index.rst new file mode 100644 index 0000000000..06ea1149d6 --- /dev/null +++ b/docs/source/cleanlab/segmentation/index.rst @@ -0,0 +1,16 @@ +segmentation +============ + +Methods to detect label issues in segmentation datasets. + +.. automodule:: cleanlab.segmentation + :autosummary: + :members: + :undoc-members: + :show-inheritance: + +.. toctree:: + + rank + filter + summary diff --git a/docs/source/cleanlab/segmentation/rank.rst b/docs/source/cleanlab/segmentation/rank.rst new file mode 100644 index 0000000000..043fd6ef82 --- /dev/null +++ b/docs/source/cleanlab/segmentation/rank.rst @@ -0,0 +1,8 @@ +rank +==== + +.. automodule:: cleanlab.segmentation.rank + :autosummary: + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/cleanlab/segmentation/summary.rst b/docs/source/cleanlab/segmentation/summary.rst new file mode 100644 index 0000000000..d03d230da8 --- /dev/null +++ b/docs/source/cleanlab/segmentation/summary.rst @@ -0,0 +1,8 @@ +summary +======= + +.. automodule:: cleanlab.segmentation.summary + :autosummary: + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/cleanlab/token_classification/index.rst b/docs/source/cleanlab/token_classification/index.rst index 1681cfa2f6..69ea1dfcde 100644 --- a/docs/source/cleanlab/token_classification/index.rst +++ b/docs/source/cleanlab/token_classification/index.rst @@ -1,6 +1,8 @@ token_classification ==================== +Methods to detect data and label issues in token classification datasets. + .. automodule:: cleanlab.token_classification :autosummary: :members: diff --git a/docs/source/conf.py b/docs/source/conf.py index 3dc11aade2..a8c19545c5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,6 +17,12 @@ sys.path.insert(0, os.path.abspath("../../cleanlab")) +# doctest setup +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. +import pathlib +sys.path.insert(0, pathlib.Path(__file__).parents[2].resolve().as_posix()) + # -- Project information ----------------------------------------------------- project = "cleanlab" @@ -40,6 +46,7 @@ "sphinx_copybutton", "sphinxcontrib.katex", "sphinx_autodoc_typehints", + 'sphinx.ext.doctest', ] numpy_show_class_members = True @@ -57,6 +64,12 @@ autosummary_generate = True +# set the default role of `name` to make cross references +default_role = "py:obj" + +# -- Options for doctest extension --------------------------------------------- +nbsphinx_allow_errors = True # to allow make doctest to run + # -- Options for apidoc extension ---------------------------------------------- # apidoc_module_dir = "cleanlab/cleanlab" @@ -147,9 +160,12 @@ # Add new tags to RELEASE_VERSIONS before release # fmt: off "RELEASE_VERSIONS": [ + "v2.4.0", + "v2.3.1", + "v2.3.0", "v2.2.0", "v2.1.0", - "v2.0.0", + "v2.0.0", "v1.0.1", ], # fmt: on @@ -178,7 +194,7 @@ .dataframe { background: #D7D7D7; } - + th { color:black; } diff --git a/docs/source/index.rst b/docs/source/index.rst index bb36ac8a0c..47a2096b83 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,9 +1,9 @@ cleanlab documentation ====================== -`cleanlab `_ **automatically finds and fixes label issues in your ML datasets.** +`cleanlab `_ **automatically detects data and label issues in your ML datasets.** -| This reduces manual work needed to fix data errors and helps train reliable ML models on noisy real-world datasets. cleanlab has already found thousands of `label errors `_ in ImageNet, MNIST, and other popular ML benchmarking datasets, so let's get started with yours! +| This helps you improve your data and train reliable ML models on noisy real-world datasets. cleanlab has already found thousands of `label errors `_ in ImageNet, MNIST, and other popular ML benchmarking datasets. Beyond handling label errors, this is a comprehensive open-source library implementing many data-centric AI capabilities. Start using automation to improve your data in 5 minutes! Quickstart ========== @@ -19,6 +19,12 @@ Quickstart pip install cleanlab + To install the package with all optional dependencies: + + .. code-block:: bash + + pip install "cleanlab[all]" + .. tab:: conda .. code-block:: bash @@ -31,45 +37,49 @@ Quickstart pip install git+https://github.com/cleanlab/cleanlab.git + To install the package with all optional dependencies: + + .. code-block:: bash + + pip install "git+https://github.com/cleanlab/cleanlab.git#egg=cleanlab[all]" + + +2. Find common issues in your data +---------------------------------- + +cleanlab automatically detects various issues in *any dataset that a classifier can be trained on*. The cleanlab package *works with any ML model* by operating on model outputs (predicted class probabilities or feature embeddings) -- it doesn't require that a particular model created those outputs. For any classification dataset, use your trained model to produce `pred_probs` (predicted class probabilities) and/or `feature_embeddings` (numeric vector representations of each datapoint). Then, these few lines of code can detect common real-world issues in your dataset like label errors, outliers, near duplicates, etc: + +.. code-block:: python + + from cleanlab import Datalab + + lab = Datalab(data=your_dataset, label_name="column_name_of_labels") + lab.find_issues(features=feature_embeddings, pred_probs=pred_probs) + lab.report() # summarize issues in dataset, how severe they are, ... -2. Find label errors in your data ---------------------------------- -cleanlab finds issues in *any dataset that a classifier can be trained on*. The cleanlab package *works with any model* by using model outputs (predicted probabilities) as input -- it doesn't depend on which model created those outputs. +3. Handle label errors and train robust models with noisy labels +---------------------------------------------------------------- -If you're using a scikit-learn-compatible model (option 1), you don't need to train a model -- you can pass the model, data, and labels into :py:meth:`CleanLearning.find_label_issues ` and cleanlab will handle model training for you. If you want to use any non-sklearn-compatible model (option 2), you can input the trained model's out-of-sample predicted probabilities into :py:meth:`find_label_issues `. Examples for both options are below. +Mislabeled data is a particularly concerning issue plaguing real-world datasets. To use a scikit-learn-compatible model for classification with noisy labels, you don't need to train a model to find label issues -- you can pass the untrained model object, data, and labels into :py:meth:`CleanLearning.find_label_issues ` and cleanlab will handle model training for you. .. code-block:: python from cleanlab.classification import CleanLearning - from cleanlab.filter import find_label_issues - # Option 1 - works with sklearn-compatible models - just input the data and labels ツ + # This works with any sklearn-compatible model - just input data + labels and cleanlab will detect label issues ツ label_issues_info = CleanLearning(clf=sklearn_compatible_model).find_label_issues(data, labels) - # Option 2 - works with ANY ML model - just input the model's predicted probabilities - ordered_label_issues = find_label_issues( - labels=labels, - pred_probs=pred_probs, # predicted probabilities from any model (ideally out-of-sample predictions) - return_indices_ranked_by='self_confidence', - ) +:py:class:`CleanLearning ` also works with models from most standard ML frameworks by wrapping the model for scikit-learn compliance, e.g. `tensorflow/keras `_ (using our KerasWrapperModel), `pytorch `_ (using skorch package), etc. -:py:class:`CleanLearning ` (option 1) also works with models from most standard ML frameworks by wrapping the model for scikit-learn compliance, e.g. huggingface/tensorflow/keras (using our KerasWrapperModel), pytorch (using skorch package), etc. +:py:meth:`find_label_issues ` returns a boolean mask flagging which examples have label issues and a numeric label quality score for each example quantifying our confidence that its label is correct. -By default, :py:meth:`find_label_issues ` returns a boolean mask of label issues. You can instead return the indices of potential mislabeled examples by setting `return_indices_ranked_by` in :py:meth:`find_label_issues `. The indices are ordered by likelihood of a label error (estimated via :py:meth:`rank.get_label_quality_scores `). +Beyond standard classification tasks, cleanlab can also detect mislabeled examples in: `multi-label data `_ (e.g. image/document tagging), `sequence prediction `_ (e.g. entity recognition), and `data labeled by multiple annotators `_ (e.g. crowdsourcing). .. important:: Cleanlab performs better if the ``pred_probs`` from your model are **out-of-sample**. Details on how to compute out-of-sample predicted probabilities for your entire dataset are :ref:`here `. -.. - TODO - include the url for tf and torch beginner tutorials - -3. Train robust models with noisy labels ----------------------------------------- - -cleanlab's :py:class:`CleanLearning ` class adapts any existing (`scikit-learn `_ `compatible `_) classification model, `clf`, to a more reliable one by allowing it to train directly on partially mislabeled datasets. - -When the :py:meth:`.fit() ` method is called, it automatically removes any examples identified as "noisy" in the provided dataset and returns a model trained only on the clean data. +cleanlab's :py:class:`CleanLearning ` class trains a more robust version of any existing (`scikit-learn `_ `compatible `_) classification model, `clf`, by fitting it to an automatically filtered version of your dataset with low-quality data removed. It returns a model trained only on the clean data, from which you can get predictions in the same way as your existing classifier. .. code-block:: python @@ -79,16 +89,16 @@ When the :py:meth:`.fit() ` method is cl = CleanLearning(clf=LogisticRegression()) # any sklearn-compatible classifier cl.fit(train_data, labels) - # Estimate the predictions you would have gotten if you trained without mislabeled data. + # Estimate the predictions you would have gotten if you trained without mislabeled data predictions = cl.predict(test_data) 4. Dataset curation: fix dataset-level issues --------------------------------------------- -cleanlab's :py:mod:`dataset ` module helps you deal with dataset-level issues by :ref:`finding overlapping classes ` (classes to merge), :ref:`rank class-level label quality ` (classes to keep/delete), and :ref:`measure overall dataset health ` (to track dataset quality as you make adjustments). +cleanlab's `dataset `_ module helps you deal with dataset-level issues -- :py:meth:`find overlapping classes ` (classes to merge), :py:meth:`rank class-level label quality ` (classes to keep/delete), and :py:meth:`measure overall dataset health ` (to track dataset quality as you make adjustments). -The example below shows how to view all dataset-level issues in one line of code with :py:meth:`dataset.health_summary() `. Check out the dataset tutorial for more examples. +View all dataset-level issues in one line of code with :py:meth:`dataset.health_summary() `. .. code-block:: python @@ -97,6 +107,18 @@ The example below shows how to view all dataset-level issues in one line of code health_summary(labels, pred_probs, class_names=class_names) +5. Improve your data via many other techniques +---------------------------------------------- + +Beyond handling label errors, cleanlab supports other data-centric AI capabilities including: + +- Detecting outliers and out-of-distribution examples in both training and future test data `(tutorial) `_ +- Analyzing data labeled by multiple annotators to estimate consensus labels and their quality `(tutorial) `_ +- Active learning with multiple annotators to identify which data is most informative to label or re-label next `(tutorial) `_ + + +If you have questions, check out our `FAQ `_ and feel free to ask in `Slack `_! + Contributing ------------ @@ -114,16 +136,21 @@ Please see our `contributing guidelines Workflows of Data-Centric AI Image Classification (pytorch) - Text Classification (tensorflow) + Text Classification (transformers) Tabular Classification (sklearn) Audio Classification (speechbrain) + Object Detection (detectron2) Find Dataset-level Issues + Regression Identifying Outliers (pytorch) Improving Consensus Labels for Multiannotator Data Multi-Label Classification Token Classification (text) + Semantic Segmentation (pytorch) + Object Detection (detectron2) Predicted Probabilities via Cross Validation FAQ @@ -139,9 +166,14 @@ Please see our `contributing guidelines PyPI Conda - Cleanlab Studio + Cleanlab Studio diff --git a/docs/source/tutorials/audio.ipynb b/docs/source/tutorials/audio.ipynb index bf013d21f2..9c45caa18b 100644 --- a/docs/source/tutorials/audio.ipynb +++ b/docs/source/tutorials/audio.ipynb @@ -1,6 +1,7 @@ { "cells": [ { + "attachments": {}, "cell_type": "markdown", "metadata": { "id": "eVufWTY3jRPx" @@ -16,7 +17,7 @@ "\n", "- Train a cross-validated linear model using the extracted features and generate out-of-sample predicted probabilities.\n", "\n", - "- Use cleanlab to identify a list of audio clips with potential label errors.\n" + "- Apply cleanlab's `Datalab` audit to these predictions in order to identify which audio clips in the dataset are likely mislabeled.\n" ] }, { @@ -27,22 +28,20 @@ "Quickstart\n", "
\n", " \n", - "Already have a `model`? Run cross-validation to get out-of-sample `pred_probs` and then the code below to get label issue indices ranked by their inferred severity.\n", + "Already have a `model`? Run cross-validation to get out-of-sample `pred_probs`, and then run the code below to audit your dataset and identify any potential issues.\n", "\n", "\n", "
\n", " \n", "```python\n", "\n", - "from cleanlab.filter import find_label_issues\n", + "from cleanlab import Datalab\n", "\n", - "ranked_label_issues = find_label_issues(\n", - " labels,\n", - " pred_probs,\n", - " return_indices_ranked_by=\"self_confidence\",\n", - ")\n", - " \n", + "lab = Datalab(data=your_dataset, label_name=\"column_name_of_labels\")\n", + "lab.find_issues(pred_probs=your_pred_probs, issue_types={\"label\":{}})\n", "\n", + "lab.get_issues(\"label\")\n", + " \n", "```\n", " \n", "
\n", @@ -68,7 +67,7 @@ "\n", "```ipython3\n", "!pip install speechbrain tensorflow sklearn tensorflow_io\n", - "!pip install cleanlab\n", + "!pip install \"cleanlab[datalab]\"\n", "# Make sure to install the version corresponding to this tutorial\n", "# E.g. if viewing master branch documentation:\n", "# !pip install git+https://github.com/cleanlab/cleanlab.git\n", @@ -86,7 +85,7 @@ "# Package installation (hidden on docs website).\n", "# Package versions used: tensorflow==2.9.1 speechbrain==0.5.12 tensorflow-io==0.26.0 torch==1.11.0 torchaudio==0.11.0\n", "\n", - "dependencies = [\"cleanlab\", \"sklearn\", \"speechbrain==0.5.12\", \"tensorflow==2.9.1\", \"tensorflow_io==0.26.0\", \"huggingface_hub==0.7.0\"]\n", + "dependencies = [\"cleanlab\", \"sklearn\", \"speechbrain==0.5.12\", \"tensorflow==2.9.1\", \"tensorflow_io==0.26.0\", \"huggingface_hub==0.7.0\", \"datasets\"]\n", "\n", "# Supress outputs that may appear if tensorflow happens to be improperly installed: \n", "import os \n", @@ -134,6 +133,8 @@ "import tensorflow as tf\n", "import torch\n", "\n", + "from cleanlab import Datalab\n", + "\n", "SEED = 456 # ensure reproducibility" ] }, @@ -222,7 +223,6 @@ "DATA_PATH = \"spoken_digits/free-spoken-digit-dataset-1.0.9/recordings/\"\n", "\n", "# Get list of .wav file names\n", - "#\n", "# os.listdir order is nondeterministic, so for reproducibility,\n", "# we sort first and then do a deterministic shuffle\n", "file_names = sorted(i for i in os.listdir(DATA_PATH) if i.endswith(\".wav\"))\n", @@ -391,8 +391,6 @@ "# Create dataframe with .wav file names\n", "df = pd.DataFrame(file_paths, columns=[\"wav_audio_file_path\"])\n", "df[\"label\"] = df.wav_audio_file_path.map(lambda x: int(Path(x).parts[-1].split(\"_\")[0]))\n", - "# Note: Classes must be represented as integer indices 0, 1, ..., num_classes - 1\n", - "# Eg. for dataset with 7 examples from 3 classes, labels might be: np.array([2,0,0,1,2,0,1])\n", "df.head(3)" ] }, @@ -541,30 +539,77 @@ "id": "laui-jXMm6qR" }, "source": [ - "Based on the given labels and out-of-sample predicted probabilities, cleanlab can quickly help us identify label issues in our dataset. For a dataset with N examples from K classes, the labels should be a 1D array of length N and predicted probabilities should be a 2D (N x K) array. Here we request that the indices of the identified label issues should be sorted by cleanlab's _self-confidence_ score, which measures the quality of each given label via the probability assigned it in our model's prediction." + "Based on the given labels, out-of-sample predicted probabilities and features, cleanlab can quickly help us identify label issues in our dataset. For a dataset with N examples from K classes, the labels should be a 1D array of length N and predicted probabilities should be a 2D (N x K) array. \n", + "\n", + "Here, we use cleanlab to find potential label errors in our data. `Datalab` has several ways of loading the data. In this case, we can just pass the DataFrame created above to instantiate the object. We will then pass in the predicted probabilites to the `find_issues()` method so that Datalab can use them to find potential label errors in our data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lab = Datalab(df, label_name=\"label\")\n", + "lab.find_issues(pred_probs=pred_probs, issue_types={\"label\":{}})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can view the results of running Datalab by calling the `report` method:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "HIlBwv17gJvH", - "outputId": "136accc8-7fa6-4d54-b30c-5df7c5def0f6" + "scrolled": true }, "outputs": [], "source": [ - "import cleanlab\n", + "lab.report()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We observe from the report that cleanlab has found some label issues in our dataset. Let us investigate these examples further.\n", "\n", - "label_issues_indices = cleanlab.filter.find_label_issues(\n", - " labels=df.label.values,\n", - " pred_probs=pred_probs,\n", - " return_indices_ranked_by=\"self_confidence\", # ranks the label issues\n", - ")\n", + "We can view the more details about the label quality for each example using the `get_issues` method, specifying `label` as the issue type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "label_issues = lab.get_issues(\"label\")\n", + "label_issues.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This method returns a dataframe containing a label quality score for each example. These numeric scores lie between 0 and 1, where lower scores indicate examples more likely to be mislabeled. The dataframe also contains a boolean column specifying whether or not each example is identified to have a label issue (indicating it is likely mislabeled).\n", + "\n", + "We can then filter for the examples that have been identified as a label error:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "identified_label_issues = label_issues[label_issues[\"is_label_issue\"] == True]\n", + "lowest_quality_labels = identified_label_issues.sort_values(\"label_score\").index\n", "\n", - "print(label_issues_indices)" + "print(f\"Here are indices of the most likely errors: \\n {lowest_quality_labels.values}\")" ] }, { @@ -573,7 +618,7 @@ "id": "iI07jQ0BnTgt" }, "source": [ - "The examples flagged by cleanlab are those worth inspecting more closely." + "These examples flagged by cleanlab are those worth inspecting more closely." ] }, { @@ -589,7 +634,7 @@ }, "outputs": [], "source": [ - "df.iloc[label_issues_indices]" + "df.iloc[lowest_quality_labels]" ] }, { @@ -673,14 +718,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 92 - }, - "id": "qeIrpgYj6zIQ", - "outputId": "2dde7439-9493-4dee-bb8f-dc5bc337378c" - }, + "metadata": {}, "outputs": [], "source": [ "wav_file_name_example = \"spoken_digits/free-spoken-digit-dataset-1.0.9/recordings/6_nicolas_8.wav\"\n", @@ -707,7 +745,7 @@ "# Note: This cell is only for docs.cleanlab.ai, if running on local Jupyter or Colab, please ignore it.\n", "\n", "highlighted_indices = [1946, 516, 469, 2132] # verify these examples were found in find_label_issues\n", - "if not all(x in label_issues_indices for x in highlighted_indices):\n", + "if not all(x in lowest_quality_labels for x in highlighted_indices):\n", " raise Exception(\"Some highlighted examples are missing from label_issues_indices.\")" ] } @@ -734,7 +772,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.10.8" } }, "nbformat": 4, diff --git a/docs/source/tutorials/datalab/datalab_advanced.ipynb b/docs/source/tutorials/datalab/datalab_advanced.ipynb new file mode 100644 index 0000000000..f3980aff04 --- /dev/null +++ b/docs/source/tutorials/datalab/datalab_advanced.ipynb @@ -0,0 +1,808 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Datalab: Advanced workflows to audit your data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Cleanlab offers a `Datalab` object to identify various issues in your machine learning datasets that may negatively impact models if not addressed. By default, `Datalab` can help you identify noisy labels, outliers, (near) duplicates, and other types of problems that commonly occur in real-world data.\n", + "\n", + "`Datalab` performs these checks by utilizing the (probabilistic) predictions from *any* ML model that has already been trained or its learned representations of the data. Underneath the hood, this class calls all the appropriate cleanlab methods for your dataset and provided model outputs, in order to best audit the data and alert you of important issues. This makes it easy to apply many functionalities of this library all within a single line of code. \n", + "\n", + "**This tutorial will demonstrate some advanced functionalities of Datalab including:**\n", + "\n", + "- Incremental issue search\n", + "- Specifying nondefault arguments to issue checks\n", + "- Save and load Datalab objects\n", + "- Adding a custom IssueManager\n", + "\n", + "If you are new to `Datalab`, check out this [quickstart tutorial](datalab_quickstart.html) for a 5-min introduction!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "Quickstart\n", + "
\n", + " \n", + "Already have (out-of-sample) `pred_probs` from a model trained on an existing set of labels? Maybe you have some `features` as well? Run the code below to examine your dataset for multiple types of issues.\n", + "\n", + "
\n", + " \n", + "```ipython3 \n", + "from cleanlab import Datalab\n", + "\n", + "lab = Datalab(data=your_dataset, label_name=\"column_name_of_labels\")\n", + "lab.find_issues(features=your_feature_matrix, pred_probs=your_pred_probs)\n", + "\n", + "lab.report()\n", + "```\n", + " \n", + "
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Install and import required dependencies" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`Datalab` has additional dependencies that are not included in the standard installation of cleanlab.\n", + "\n", + "You can use pip to install all packages required for this tutorial as follows:\n", + "\n", + "```ipython3\n", + "!pip install matplotlib \n", + "!pip install \"cleanlab[datalab]\"\n", + "\n", + "# Make sure to install the version corresponding to this tutorial\n", + "# E.g. if viewing master branch documentation:\n", + "# !pip install git+https://github.com/cleanlab/cleanlab.git\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Package installation (hidden on docs website).\n", + "dependencies = [\"cleanlab\", \"matplotlib\", \"datasets\"] # TODO: make sure this list is updated\n", + "\n", + "if \"google.colab\" in str(get_ipython()): # Check if it's running in Google Colab\n", + " %pip install cleanlab # for colab\n", + " cmd = ' '.join([dep for dep in dependencies if dep != \"cleanlab\"])\n", + " %pip install $cmd\n", + "else:\n", + " missing_dependencies = []\n", + " for dependency in dependencies:\n", + " try:\n", + " __import__(dependency)\n", + " except ImportError:\n", + " missing_dependencies.append(dependency)\n", + "\n", + " if len(missing_dependencies) > 0:\n", + " print(\"Missing required dependencies:\")\n", + " print(*missing_dependencies, sep=\", \")\n", + " print(\"\\nPlease install them before running the rest of this notebook.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "from sklearn.linear_model import LogisticRegression\n", + "from sklearn.model_selection import cross_val_predict\n", + "\n", + "from cleanlab import Datalab" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create and load the data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We'll load a toy classification dataset for this tutorial. The dataset has two numerical features and a label column with three classes." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
See the code for data generation. **(click to expand)**\n", + " \n", + "```ipython3\n", + "# Note: This pulldown content is for docs.cleanlab.ai, if running on local Jupyter or Colab, please ignore it.\n", + "\n", + "from sklearn.model_selection import train_test_split\n", + "from cleanlab.benchmarking.noise_generation import (\n", + " generate_noise_matrix_from_trace,\n", + " generate_noisy_labels,\n", + ")\n", + "\n", + "SEED = 123\n", + "np.random.seed(SEED)\n", + "\n", + "BINS = {\n", + " \"low\": [-np.inf, 3.3],\n", + " \"mid\": [3.3, 6.6],\n", + " \"high\": [6.6, +np.inf],\n", + "}\n", + "\n", + "BINS_MAP = {\n", + " \"low\": 0,\n", + " \"mid\": 1,\n", + " \"high\": 2,\n", + "}\n", + "\n", + "\n", + "def create_data():\n", + "\n", + " X = np.random.rand(250, 2) * 5\n", + " y = np.sum(X, axis=1)\n", + " # Map y to bins based on the BINS dict\n", + " y_bin = np.array([k for y_i in y for k, v in BINS.items() if v[0] <= y_i < v[1]])\n", + " y_bin_idx = np.array([BINS_MAP[k] for k in y_bin])\n", + "\n", + " # Split into train and test\n", + " X_train, X_test, y_train, y_test, y_train_idx, y_test_idx = train_test_split(\n", + " X, y_bin, y_bin_idx, test_size=0.5, random_state=SEED\n", + " )\n", + "\n", + " # Add several (5) out-of-distribution points. Sliding them along the decision boundaries\n", + " # to make them look like they are out-of-frame\n", + " X_out = np.array(\n", + " [\n", + " [-1.5, 3.0],\n", + " [-1.75, 6.5],\n", + " [1.5, 7.2],\n", + " [2.5, -2.0],\n", + " [5.5, 7.0],\n", + " ]\n", + " )\n", + " # Add a near duplicate point to the last outlier, with some tiny noise added\n", + " near_duplicate = X_out[-1:] + np.random.rand(1, 2) * 1e-6\n", + " X_out = np.concatenate([X_out, near_duplicate])\n", + "\n", + " y_out = np.sum(X_out, axis=1)\n", + " y_out_bin = np.array([k for y_i in y_out for k, v in BINS.items() if v[0] <= y_i < v[1]])\n", + " y_out_bin_idx = np.array([BINS_MAP[k] for k in y_out_bin])\n", + "\n", + " # Add to train\n", + " X_train = np.concatenate([X_train, X_out])\n", + " y_train = np.concatenate([y_train, y_out])\n", + " y_train_idx = np.concatenate([y_train_idx, y_out_bin_idx])\n", + "\n", + " # Add an exact duplicate example to the training set\n", + " exact_duplicate_idx = np.random.randint(0, len(X_train))\n", + " X_duplicate = X_train[exact_duplicate_idx, None]\n", + " y_duplicate = y_train[exact_duplicate_idx, None]\n", + " y_duplicate_idx = y_train_idx[exact_duplicate_idx, None]\n", + "\n", + " # Add to train\n", + " X_train = np.concatenate([X_train, X_duplicate])\n", + " y_train = np.concatenate([y_train, y_duplicate])\n", + " y_train_idx = np.concatenate([y_train_idx, y_duplicate_idx])\n", + "\n", + " py = np.bincount(y_train_idx) / float(len(y_train_idx))\n", + " m = len(BINS)\n", + "\n", + " noise_matrix = generate_noise_matrix_from_trace(\n", + " m,\n", + " trace=0.9 * m,\n", + " py=py,\n", + " valid_noise_matrix=True,\n", + " seed=SEED,\n", + " )\n", + "\n", + " noisy_labels_idx = generate_noisy_labels(y_train_idx, noise_matrix)\n", + " noisy_labels = np.array([list(BINS_MAP.keys())[i] for i in noisy_labels_idx])\n", + "\n", + " return X_train, y_train_idx, noisy_labels, noisy_labels_idx, X_out, X_duplicate\n", + "```\n", + "\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "from sklearn.model_selection import train_test_split\n", + "from cleanlab.benchmarking.noise_generation import (\n", + " generate_noise_matrix_from_trace,\n", + " generate_noisy_labels,\n", + ")\n", + "\n", + "SEED = 123\n", + "np.random.seed(SEED)\n", + "\n", + "BINS = {\n", + " \"low\": [-np.inf, 3.3],\n", + " \"mid\": [3.3, 6.6],\n", + " \"high\": [6.6, +np.inf],\n", + "}\n", + "\n", + "BINS_MAP = {\n", + " \"low\": 0,\n", + " \"mid\": 1,\n", + " \"high\": 2,\n", + "}\n", + "\n", + "\n", + "def create_data():\n", + "\n", + " X = np.random.rand(250, 2) * 5\n", + " y = np.sum(X, axis=1)\n", + " # Map y to bins based on the BINS dict\n", + " y_bin = np.array([k for y_i in y for k, v in BINS.items() if v[0] <= y_i < v[1]])\n", + " y_bin_idx = np.array([BINS_MAP[k] for k in y_bin])\n", + "\n", + " # Split into train and test\n", + " X_train, X_test, y_train, y_test, y_train_idx, y_test_idx = train_test_split(\n", + " X, y_bin, y_bin_idx, test_size=0.5, random_state=SEED\n", + " )\n", + "\n", + " # Add several (5) out-of-distribution points. Sliding them along the decision boundaries\n", + " # to make them look like they are out-of-frame\n", + " X_out = np.array(\n", + " [\n", + " [-1.5, 3.0],\n", + " [-1.75, 6.5],\n", + " [1.5, 7.2],\n", + " [2.5, -2.0],\n", + " [5.5, 7.0],\n", + " ]\n", + " )\n", + " # Add a near duplicate point to the last outlier, with some tiny noise added\n", + " near_duplicate = X_out[-1:] + np.random.rand(1, 2) * 1e-6\n", + " X_out = np.concatenate([X_out, near_duplicate])\n", + "\n", + " y_out = np.sum(X_out, axis=1)\n", + " y_out_bin = np.array([k for y_i in y_out for k, v in BINS.items() if v[0] <= y_i < v[1]])\n", + " y_out_bin_idx = np.array([BINS_MAP[k] for k in y_out_bin])\n", + "\n", + " # Add to train\n", + " X_train = np.concatenate([X_train, X_out])\n", + " y_train = np.concatenate([y_train, y_out])\n", + " y_train_idx = np.concatenate([y_train_idx, y_out_bin_idx])\n", + "\n", + " # Add an exact duplicate example to the training set\n", + " exact_duplicate_idx = np.random.randint(0, len(X_train))\n", + " X_duplicate = X_train[exact_duplicate_idx, None]\n", + " y_duplicate = y_train[exact_duplicate_idx, None]\n", + " y_duplicate_idx = y_train_idx[exact_duplicate_idx, None]\n", + "\n", + " # Add to train\n", + " X_train = np.concatenate([X_train, X_duplicate])\n", + " y_train = np.concatenate([y_train, y_duplicate])\n", + " y_train_idx = np.concatenate([y_train_idx, y_duplicate_idx])\n", + "\n", + " py = np.bincount(y_train_idx) / float(len(y_train_idx))\n", + " m = len(BINS)\n", + "\n", + " noise_matrix = generate_noise_matrix_from_trace(\n", + " m,\n", + " trace=0.9 * m,\n", + " py=py,\n", + " valid_noise_matrix=True,\n", + " seed=SEED,\n", + " )\n", + "\n", + " noisy_labels_idx = generate_noisy_labels(y_train_idx, noise_matrix)\n", + " noisy_labels = np.array([list(BINS_MAP.keys())[i] for i in noisy_labels_idx])\n", + "\n", + " return X_train, y_train_idx, noisy_labels, noisy_labels_idx, X_out, X_duplicate" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "X_train, y_train_idx, noisy_labels, noisy_labels_idx, X_out, X_duplicate = create_data()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We make a scatter plot of the features, with a color corresponding to the observed labels. Incorrect given labels are highlighted in red if they do not match the true label, outliers highlighted with an a black cross, and duplicates highlighted with a cyan cross." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
See the code to visualize the data. **(click to expand)**\n", + " \n", + "```ipython3\n", + "# Note: This pulldown content is for docs.cleanlab.ai, if running on local Jupyter or Colab, please ignore it.\n", + "\n", + "import matplotlib.pyplot as plt\n", + "\n", + "def plot_data(X_train, y_train_idx, noisy_labels_idx, X_out, X_duplicate):\n", + " # Plot data with clean labels and noisy labels, use BINS_MAP for the legend\n", + " fig, ax = plt.subplots(figsize=(8, 6.5))\n", + " \n", + " low = ax.scatter(X_train[noisy_labels_idx == 0, 0], X_train[noisy_labels_idx == 0, 1], label=\"low\")\n", + " mid = ax.scatter(X_train[noisy_labels_idx == 1, 0], X_train[noisy_labels_idx == 1, 1], label=\"mid\")\n", + " high = ax.scatter(X_train[noisy_labels_idx == 2, 0], X_train[noisy_labels_idx == 2, 1], label=\"high\")\n", + " \n", + " ax.set_title(\"Noisy labels\")\n", + " ax.set_xlabel(r\"$x_1$\", fontsize=16)\n", + " ax.set_ylabel(r\"$x_2$\", fontsize=16)\n", + "\n", + " # Plot true boundaries (x+y=3.3, x+y=6.6)\n", + " ax.set_xlim(-3.5, 9.0)\n", + " ax.set_ylim(-3.5, 9.0)\n", + " ax.plot([-0.7, 4.0], [4.0, -0.7], color=\"k\", linestyle=\"--\", alpha=0.5)\n", + " ax.plot([-0.7, 7.3], [7.3, -0.7], color=\"k\", linestyle=\"--\", alpha=0.5)\n", + "\n", + " # Draw red circles around the points that are misclassified (i.e. the points that are in the wrong bin)\n", + " for i, (X, y) in enumerate(zip([X_train, X_train], [y_train_idx, noisy_labels_idx])):\n", + " for j, (k, v) in enumerate(BINS_MAP.items()):\n", + " label_err = ax.scatter(\n", + " X[(y == v) & (y != y_train_idx), 0],\n", + " X[(y == v) & (y != y_train_idx), 1],\n", + " s=180,\n", + " marker=\"o\",\n", + " facecolor=\"none\",\n", + " edgecolors=\"red\",\n", + " linewidths=2.5,\n", + " alpha=0.5,\n", + " label=\"Label error\",\n", + " )\n", + "\n", + "\n", + " outlier = ax.scatter(X_out[:, 0], X_out[:, 1], color=\"k\", marker=\"x\", s=100, linewidth=2, label=\"Outlier\")\n", + "\n", + " # Plot the exact duplicate\n", + " dups = ax.scatter(\n", + " X_duplicate[:, 0],\n", + " X_duplicate[:, 1],\n", + " color=\"c\",\n", + " marker=\"x\",\n", + " s=100,\n", + " linewidth=2,\n", + " label=\"Duplicates\",\n", + " )\n", + " \n", + " first_legend = ax.legend(handles=[low, mid, high], loc=[0.75, 0.7], title=\"Given Class Label\", alignment=\"left\", title_fontproperties={\"weight\":\"semibold\"})\n", + " second_legend = ax.legend(handles=[label_err, outlier, dups], loc=[0.75, 0.45], title=\"Type of Issue\", alignment=\"left\", title_fontproperties={\"weight\":\"semibold\"})\n", + " \n", + " ax = plt.gca().add_artist(first_legend)\n", + " ax = plt.gca().add_artist(second_legend)\n", + " plt.tight_layout()\n", + "```\n", + " \n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "def plot_data(X_train, y_train_idx, noisy_labels_idx, X_out, X_duplicate):\n", + " # Plot data with clean labels and noisy labels, use BINS_MAP for the legend\n", + " fig, ax = plt.subplots(figsize=(6, 4))\n", + " \n", + " low = ax.scatter(X_train[noisy_labels_idx == 0, 0], X_train[noisy_labels_idx == 0, 1], label=\"low\")\n", + " mid = ax.scatter(X_train[noisy_labels_idx == 1, 0], X_train[noisy_labels_idx == 1, 1], label=\"mid\")\n", + " high = ax.scatter(X_train[noisy_labels_idx == 2, 0], X_train[noisy_labels_idx == 2, 1], label=\"high\")\n", + " \n", + " ax.set_title(\"Noisy labels\")\n", + " ax.set_xlabel(r\"$x_1$\", fontsize=16)\n", + " ax.set_ylabel(r\"$x_2$\", fontsize=16)\n", + "\n", + " # Plot true boundaries (x+y=3.3, x+y=6.6)\n", + " ax.set_xlim(-2.5, 8.5)\n", + " ax.set_ylim(-3.5, 9.0)\n", + " ax.plot([-0.7, 4.0], [4.0, -0.7], color=\"k\", linestyle=\"--\", alpha=0.5)\n", + " ax.plot([-0.7, 7.3], [7.3, -0.7], color=\"k\", linestyle=\"--\", alpha=0.5)\n", + "\n", + " # Draw red circles around the points that are misclassified (i.e. the points that are in the wrong bin)\n", + " for i, (X, y) in enumerate(zip([X_train, X_train], [y_train_idx, noisy_labels_idx])):\n", + " for j, (k, v) in enumerate(BINS_MAP.items()):\n", + " label_err = ax.scatter(\n", + " X[(y == v) & (y != y_train_idx), 0],\n", + " X[(y == v) & (y != y_train_idx), 1],\n", + " s=180,\n", + " marker=\"o\",\n", + " facecolor=\"none\",\n", + " edgecolors=\"red\",\n", + " linewidths=2.5,\n", + " alpha=0.5,\n", + " label=\"Label error\",\n", + " )\n", + "\n", + "\n", + " outlier = ax.scatter(X_out[:, 0], X_out[:, 1], color=\"k\", marker=\"x\", s=100, linewidth=2, label=\"Outlier\")\n", + "\n", + " # Plot the exact duplicate\n", + " dups = ax.scatter(\n", + " X_duplicate[:, 0],\n", + " X_duplicate[:, 1],\n", + " color=\"c\",\n", + " marker=\"x\",\n", + " s=100,\n", + " linewidth=2,\n", + " label=\"Duplicates\",\n", + " )\n", + " \n", + " title_fontproperties = {\"weight\":\"semibold\", \"size\": 8}\n", + " first_legend = ax.legend(handles=[low, mid, high], loc=[0.76, 0.7], title=\"Given Class Label\", alignment=\"left\", title_fontproperties=title_fontproperties, fontsize=8, markerscale=0.5)\n", + " second_legend = ax.legend(handles=[label_err, outlier, dups], loc=[0.76, 0.46], title=\"Type of Issue\", alignment=\"left\", title_fontproperties=title_fontproperties, fontsize=8, markerscale=0.5)\n", + " \n", + " ax = plt.gca().add_artist(first_legend)\n", + " ax = plt.gca().add_artist(second_legend)\n", + " plt.tight_layout()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot_data(X_train, y_train_idx, noisy_labels_idx, X_out, X_duplicate)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In real-world scenarios, you won't know the true labels or the distribution of the features, so we won't use these in this tutorial, except for evaluation purposes." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Get out-of-sample predicted probabilities from a classifier" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To detect certain types of issues in classification data (e.g. label errors), `Datalab` relies on predicted class probabilities from a trained model. Ideally, the prediction for each example should be out-of-sample (to avoid overfitting), coming from a copy of the model that was not trained on this example. \n", + "\n", + "This tutorial uses a simple logistic regression model \n", + "and the `cross_val_predict()` function from scikit-learn to generate out-of-sample predicted class probabilities for every example in the training set. You can replace this with *any* other classifier model and train it with cross-validation to get out-of-sample predictions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model = LogisticRegression()\n", + "pred_probs = cross_val_predict(\n", + " estimator=model, X=X_train, y=noisy_labels, cv=5, method=\"predict_proba\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Instantiate Datalab object" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we instantiate the Datalab object that will be used in the remainder in the tutorial by passing in the data created above.\n", + "\n", + "`Datalab` has several ways of loading the data. In this case, we'll simply wrap the training features and noisy labels in a dictionary so that we can pass it to `Datalab`.\n", + "\n", + "Other supported data formats for `Datalab` include: [HuggingFace Datasets](https://huggingface.co/docs/datasets/index) and [pandas DataFrame](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html). `Datalab` works across most data modalities (image, text, tabular, audio, etc). It is intended to find issues that commonly occur in datasets for which you have trained a supervised ML model, regardless of the type of data.\n", + "\n", + "Currently, pandas DataFrames that contain categorical columns might cause some issues when instantiating the `Datalab` object, so it is recommended to ensure that your DataFrame does not contain any categorical columns, or use other data formats (eg. python dictionary, HuggingFace Datasets) to pass in your data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = {\"X\": X_train, \"y\": noisy_labels}\n", + "\n", + "lab = Datalab(data, label_name=\"y\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Functionality 1**: Incremental issue search " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can call `find_issues` multiple times on a `Datalab` object to detect issues one type at a time.\n", + "\n", + "This is done via the `issue_types` argument which accepts a dictionary of issue types and any corresponding keyword arguments to specify nondefault keyword arguments to use for detecting each type of issues. In this first call, we only want to detect label issues, which are detected solely based on `pred_probs`, hence there is no need for us to pass in `features` here." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lab.find_issues(pred_probs=pred_probs, issue_types={\"label\": {}}) \n", + "lab.report()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can check for additional types of issues with the same `Datalab`. Here, we would like to detect outliers and near duplicates which both utilize the features of the data.\n", + "\n", + "Notice that this second call to `find_issues()` updates the output of `report()`, we can see the existing label issues detected alongside the new issues." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lab.find_issues(features=data[\"X\"], issue_types={\"outlier\": {}, \"near_duplicate\": {}})\n", + "lab.report()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Functionality 2**: Specifying nondefault arguments" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also overwrite previously-executed checks for a type of issue. Here we re-run the detection of outliers, but specify that different non-default settings should be used (in this case, the number of neighbors `k` compared against to determine which datapoints are outliers). \n", + "The results from this new detection will replace the original outlier detection results in the updated `Datalab`. You could similarly specify non-default settings for other issue types in the first call to `Datalab.find_issues()`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lab.find_issues(features=data[\"X\"], issue_types={\"outlier\": {\"k\": 30}})\n", + "lab.report()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also increase the verbosity of the `report` to see additional information about the data issues and control how many top-ranked examples are shown for each issue." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lab.report(num_examples=10, verbosity=2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Notice how the number of flagged outlier issues has changed after specfying different settings to use for outlier detection." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Functionality 3**: Save and load Datalab objects" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A `Datalab` can be saved to a folder at a specified path. In a future Python process, this path can be used to load the `Datalab` from file back into memory. Your dataset is not saved as part of this process, so you'll need to save/load it separately to keep working with it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "path = \"datalab-files\"\n", + "lab.save(path, force=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can load a `Datalab` object we have on file and view the previously detected issues." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "new_lab = Datalab.load(path)\n", + "new_lab.report()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Functionality 4**: Adding a custom IssueManager" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`Datalab` detects pre-defined types of issues for you in one line of code: `find_issues()`. What if you want to check for other custom types of issues along with these pre-defined types, all within the same line of code?\n", + "\n", + "All issue types in `Datalab` are subclasses of cleanlab's `IssueManager` class.\n", + "To register a custom issue type for use with `Datalab`, simply also make it a subclass of `IssueManager`.\n", + "\n", + "The necessary members to implement in the subclass are:\n", + "\n", + "- A class variable called `issue_name` that acts as a unique identifier for the type of issue.\n", + "- An instance method called `find_issues` that:\n", + " - Computes a quality score for each example in the dataset (between 0-1), in terms of how *unlikely* it is to be an issue.\n", + " - Flags each example as an issue or not (may be based on thresholding the quality scores).\n", + " - Combine these in a dataframe that is assigned to an `issues` attribute of the `IssueManager`.\n", + " - Define a summary score for the overall quality of entire dataset, in terms of this type of issue. Set this score as part of the `summary` attribute of the `IssueManager`.\n", + " \n", + "To demonstrate this, we create an arbitrary issue type that checks the divisibility of an example's index in the dataset by 13." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from cleanlab.datalab.issue_manager import IssueManager\n", + "from cleanlab.datalab.factory import register\n", + "\n", + "\n", + "def scoring_function(idx: int, div: int = 13) -> float:\n", + " if idx == 0:\n", + " # Zero excluded from the divisibility check, gets the highest score\n", + " return 1\n", + " rem = idx % div\n", + " inv_scale = idx // div\n", + " if rem == 0:\n", + " return 0.5 * (1 - np.exp(-0.1*(inv_scale-1)))\n", + " else:\n", + " return 1 - 0.49 * (1 - np.exp(-inv_scale**0.5))*rem/div\n", + "\n", + "\n", + "@register # register this issue type for use with Datalab\n", + "class SuperstitionIssueManager(IssueManager):\n", + " \"\"\"A custom issue manager that keeps track of issue indices that\n", + " are divisible by 13.\n", + " \"\"\"\n", + " description: str = \"Examples with indices that are divisible by 13 may be unlucky.\" # Optional\n", + " issue_name: str = \"superstition\"\n", + "\n", + " def find_issues(self, div=13, **_) -> None:\n", + " ids = self.datalab.issues.index.to_series()\n", + " issues_mask = ids.apply(lambda idx: idx % div == 0 and idx != 0)\n", + " scores = ids.apply(lambda idx: scoring_function(idx, div))\n", + " self.issues = pd.DataFrame(\n", + " {\n", + " f\"is_{self.issue_name}_issue\": issues_mask,\n", + " self.issue_score_key: scores,\n", + " },\n", + " )\n", + " summary_score = 1 - sum(issues_mask) / len(issues_mask)\n", + " self.summary = self.make_summary(score = summary_score)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once registered, this `IssueManager` will perform custom issue checks when `find_issues` is called on a `Datalab` instance.\n", + "\n", + "As our `Datalab` instance here already has results from the outlier and near duplicate checks, we perform the custom issue check separately." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lab.find_issues(issue_types={\"superstition\": {}})\n", + "lab.report()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.8" + }, + "vscode": { + "interpreter": { + "hash": "d4d1e4263499bec80672ea0156c357c1ee493ec2b1c70f0acce89fc37c4a6abe" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/source/tutorials/datalab/datalab_quickstart.ipynb b/docs/source/tutorials/datalab/datalab_quickstart.ipynb new file mode 100644 index 0000000000..62c4e12194 --- /dev/null +++ b/docs/source/tutorials/datalab/datalab_quickstart.ipynb @@ -0,0 +1,764 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Datalab: A unified audit to detect all kinds of issues in data and labels" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Cleanlab offers a `Datalab` object that can identify various issues in your machine learning datasets, such as noisy labels, outliers, (near) duplicates, and other types of problems common in real-world data. These data issues may negatively impact models if not addressed. `Datalab` utilizes *any* ML model you have already trained for your data to diagnose these issues, it only requires access to either: (probabilistic) predictions from your model or its learned representations of the data.\n", + "\n", + "\n", + "**Overview of what we'll do in this tutorial:**\n", + "\n", + "- Compute out-of-sample predicted probabilities for a sample dataset using cross-validation.\n", + "- Use `Datalab` to identify issues such as noisy labels, outliers, (near) duplicates, and other types of problems \n", + "- View the issue summaries and other information about our sample dataset\n", + "\n", + "You can easily replace our demo dataset with your own image/text/tabular/audio/etc dataset, and then run the same code to discover what sort of issues lurk within it!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "Quickstart\n", + "
\n", + " \n", + "Already have (out-of-sample) `pred_probs` from a model trained on an existing set of labels? Maybe you also have some numeric `features` (or model embeddings of data)? Run the code below to examine your dataset for multiple types of issues.\n", + "\n", + "
\n", + " \n", + "```ipython3 \n", + "from cleanlab import Datalab\n", + "\n", + "lab = Datalab(data=your_dataset, label_name=\"column_name_of_labels\")\n", + "lab.find_issues(features=your_feature_matrix, pred_probs=your_pred_probs)\n", + "\n", + "lab.report()\n", + "```\n", + " \n", + "
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Install and import required dependencies" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`Datalab` has additional dependencies that are not included in the standard installation of cleanlab.\n", + "\n", + "You can use pip to install all packages required for this tutorial as follows:\n", + "\n", + "```ipython3\n", + "!pip install matplotlib \n", + "!pip install \"cleanlab[datalab]\"\n", + "\n", + "# Make sure to install the version corresponding to this tutorial\n", + "# E.g. if viewing master branch documentation:\n", + "# !pip install git+https://github.com/cleanlab/cleanlab.git\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Package installation (hidden on docs website).\n", + "dependencies = [\"cleanlab\", \"matplotlib\", \"datasets\"] # TODO: make sure this list is updated\n", + "\n", + "if \"google.colab\" in str(get_ipython()): # Check if it's running in Google Colab\n", + " %pip install cleanlab # for colab\n", + " cmd = ' '.join([dep for dep in dependencies if dep != \"cleanlab\"])\n", + " %pip install $cmd\n", + "else:\n", + " missing_dependencies = []\n", + " for dependency in dependencies:\n", + " try:\n", + " __import__(dependency)\n", + " except ImportError:\n", + " missing_dependencies.append(dependency)\n", + "\n", + " if len(missing_dependencies) > 0:\n", + " print(\"Missing required dependencies:\")\n", + " print(*missing_dependencies, sep=\", \")\n", + " print(\"\\nPlease install them before running the rest of this notebook.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "from sklearn.linear_model import LogisticRegression\n", + "from sklearn.model_selection import cross_val_predict\n", + "\n", + "from cleanlab import Datalab" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Create and load the data (can skip these details)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We'll load a toy classification dataset for this tutorial. The dataset has two numerical features and a label column with three possible classes. Each example is classified as either: *low*, *mid* or *high*." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
See the code for data generation. **(click to expand)**\n", + " \n", + "```ipython3\n", + "# Note: This pulldown content is for docs.cleanlab.ai, if running on local Jupyter or Colab, please ignore it.\n", + "\n", + "from sklearn.model_selection import train_test_split\n", + "from cleanlab.benchmarking.noise_generation import (\n", + " generate_noise_matrix_from_trace,\n", + " generate_noisy_labels,\n", + ")\n", + "\n", + "SEED = 123\n", + "np.random.seed(SEED)\n", + "\n", + "BINS = {\n", + " \"low\": [-np.inf, 3.3],\n", + " \"mid\": [3.3, 6.6],\n", + " \"high\": [6.6, +np.inf],\n", + "}\n", + "\n", + "BINS_MAP = {\n", + " \"low\": 0,\n", + " \"mid\": 1,\n", + " \"high\": 2,\n", + "}\n", + "\n", + "\n", + "def create_data():\n", + "\n", + " X = np.random.rand(250, 2) * 5\n", + " y = np.sum(X, axis=1)\n", + " # Map y to bins based on the BINS dict\n", + " y_bin = np.array([k for y_i in y for k, v in BINS.items() if v[0] <= y_i < v[1]])\n", + " y_bin_idx = np.array([BINS_MAP[k] for k in y_bin])\n", + "\n", + " # Split into train and test\n", + " X_train, X_test, y_train, y_test, y_train_idx, y_test_idx = train_test_split(\n", + " X, y_bin, y_bin_idx, test_size=0.5, random_state=SEED\n", + " )\n", + "\n", + " # Add several (5) out-of-distribution points. Sliding them along the decision boundaries\n", + " # to make them look like they are out-of-frame\n", + " X_out = np.array(\n", + " [\n", + " [-1.5, 3.0],\n", + " [-1.75, 6.5],\n", + " [1.5, 7.2],\n", + " [2.5, -2.0],\n", + " [5.5, 7.0],\n", + " ]\n", + " )\n", + " # Add a near duplicate point to the last outlier, with some tiny noise added\n", + " near_duplicate = X_out[-1:] + np.random.rand(1, 2) * 1e-6\n", + " X_out = np.concatenate([X_out, near_duplicate])\n", + "\n", + " y_out = np.sum(X_out, axis=1)\n", + " y_out_bin = np.array([k for y_i in y_out for k, v in BINS.items() if v[0] <= y_i < v[1]])\n", + " y_out_bin_idx = np.array([BINS_MAP[k] for k in y_out_bin])\n", + "\n", + " # Add to train\n", + " X_train = np.concatenate([X_train, X_out])\n", + " y_train = np.concatenate([y_train, y_out])\n", + " y_train_idx = np.concatenate([y_train_idx, y_out_bin_idx])\n", + "\n", + " # Add an exact duplicate example to the training set\n", + " exact_duplicate_idx = np.random.randint(0, len(X_train))\n", + " X_duplicate = X_train[exact_duplicate_idx, None]\n", + " y_duplicate = y_train[exact_duplicate_idx, None]\n", + " y_duplicate_idx = y_train_idx[exact_duplicate_idx, None]\n", + "\n", + " # Add to train\n", + " X_train = np.concatenate([X_train, X_duplicate])\n", + " y_train = np.concatenate([y_train, y_duplicate])\n", + " y_train_idx = np.concatenate([y_train_idx, y_duplicate_idx])\n", + "\n", + " py = np.bincount(y_train_idx) / float(len(y_train_idx))\n", + " m = len(BINS)\n", + "\n", + " noise_matrix = generate_noise_matrix_from_trace(\n", + " m,\n", + " trace=0.9 * m,\n", + " py=py,\n", + " valid_noise_matrix=True,\n", + " seed=SEED,\n", + " )\n", + "\n", + " noisy_labels_idx = generate_noisy_labels(y_train_idx, noise_matrix)\n", + " noisy_labels = np.array([list(BINS_MAP.keys())[i] for i in noisy_labels_idx])\n", + "\n", + " return X_train, y_train_idx, noisy_labels, noisy_labels_idx, X_out, X_duplicate\n", + "```\n", + "\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "from sklearn.model_selection import train_test_split\n", + "from cleanlab.benchmarking.noise_generation import (\n", + " generate_noise_matrix_from_trace,\n", + " generate_noisy_labels,\n", + ")\n", + "\n", + "SEED = 123\n", + "np.random.seed(SEED)\n", + "\n", + "BINS = {\n", + " \"low\": [-np.inf, 3.3],\n", + " \"mid\": [3.3, 6.6],\n", + " \"high\": [6.6, +np.inf],\n", + "}\n", + "\n", + "BINS_MAP = {\n", + " \"low\": 0,\n", + " \"mid\": 1,\n", + " \"high\": 2,\n", + "}\n", + "\n", + "\n", + "def create_data():\n", + "\n", + " X = np.random.rand(250, 2) * 5\n", + " y = np.sum(X, axis=1)\n", + " # Map y to bins based on the BINS dict\n", + " y_bin = np.array([k for y_i in y for k, v in BINS.items() if v[0] <= y_i < v[1]])\n", + " y_bin_idx = np.array([BINS_MAP[k] for k in y_bin])\n", + "\n", + " # Split into train and test\n", + " X_train, X_test, y_train, y_test, y_train_idx, y_test_idx = train_test_split(\n", + " X, y_bin, y_bin_idx, test_size=0.5, random_state=SEED\n", + " )\n", + "\n", + " # Add several (5) out-of-distribution points. Sliding them along the decision boundaries\n", + " # to make them look like they are out-of-frame\n", + " X_out = np.array(\n", + " [\n", + " [-1.5, 3.0],\n", + " [-1.75, 6.5],\n", + " [1.5, 7.2],\n", + " [2.5, -2.0],\n", + " [5.5, 7.0],\n", + " ]\n", + " )\n", + " # Add a near duplicate point to the last outlier, with some tiny noise added\n", + " near_duplicate = X_out[-1:] + np.random.rand(1, 2) * 1e-6\n", + " X_out = np.concatenate([X_out, near_duplicate])\n", + "\n", + " y_out = np.sum(X_out, axis=1)\n", + " y_out_bin = np.array([k for y_i in y_out for k, v in BINS.items() if v[0] <= y_i < v[1]])\n", + " y_out_bin_idx = np.array([BINS_MAP[k] for k in y_out_bin])\n", + "\n", + " # Add to train\n", + " X_train = np.concatenate([X_train, X_out])\n", + " y_train = np.concatenate([y_train, y_out])\n", + " y_train_idx = np.concatenate([y_train_idx, y_out_bin_idx])\n", + "\n", + " # Add an exact duplicate example to the training set\n", + " exact_duplicate_idx = np.random.randint(0, len(X_train))\n", + " X_duplicate = X_train[exact_duplicate_idx, None]\n", + " y_duplicate = y_train[exact_duplicate_idx, None]\n", + " y_duplicate_idx = y_train_idx[exact_duplicate_idx, None]\n", + "\n", + " # Add to train\n", + " X_train = np.concatenate([X_train, X_duplicate])\n", + " y_train = np.concatenate([y_train, y_duplicate])\n", + " y_train_idx = np.concatenate([y_train_idx, y_duplicate_idx])\n", + "\n", + " py = np.bincount(y_train_idx) / float(len(y_train_idx))\n", + " m = len(BINS)\n", + "\n", + " noise_matrix = generate_noise_matrix_from_trace(\n", + " m,\n", + " trace=0.9 * m,\n", + " py=py,\n", + " valid_noise_matrix=True,\n", + " seed=SEED,\n", + " )\n", + "\n", + " noisy_labels_idx = generate_noisy_labels(y_train_idx, noise_matrix)\n", + " noisy_labels = np.array([list(BINS_MAP.keys())[i] for i in noisy_labels_idx])\n", + "\n", + " return X_train, y_train_idx, noisy_labels, noisy_labels_idx, X_out, X_duplicate" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "X_train, y_train_idx, noisy_labels, noisy_labels_idx, X_out, X_duplicate = create_data()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We make a scatter plot of the features, with a color corresponding to the observed labels. Incorrect given labels are highlighted in red if they do not match the true label, outliers highlighted with an a black cross, and duplicates highlighted with a cyan cross." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
See the code to visualize the data. **(click to expand)**\n", + " \n", + "```ipython3\n", + "# Note: This pulldown content is for docs.cleanlab.ai, if running on local Jupyter or Colab, please ignore it.\n", + "\n", + "import matplotlib.pyplot as plt\n", + "\n", + "def plot_data(X_train, y_train_idx, noisy_labels_idx, X_out, X_duplicate):\n", + " # Plot data with clean labels and noisy labels, use BINS_MAP for the legend\n", + " fig, ax = plt.subplots(figsize=(8, 6.5))\n", + " \n", + " low = ax.scatter(X_train[noisy_labels_idx == 0, 0], X_train[noisy_labels_idx == 0, 1], label=\"low\")\n", + " mid = ax.scatter(X_train[noisy_labels_idx == 1, 0], X_train[noisy_labels_idx == 1, 1], label=\"mid\")\n", + " high = ax.scatter(X_train[noisy_labels_idx == 2, 0], X_train[noisy_labels_idx == 2, 1], label=\"high\")\n", + " \n", + " ax.set_title(\"Noisy labels\")\n", + " ax.set_xlabel(r\"$x_1$\", fontsize=16)\n", + " ax.set_ylabel(r\"$x_2$\", fontsize=16)\n", + "\n", + " # Plot true boundaries (x+y=3.3, x+y=6.6)\n", + " ax.set_xlim(-3.5, 9.0)\n", + " ax.set_ylim(-3.5, 9.0)\n", + " ax.plot([-0.7, 4.0], [4.0, -0.7], color=\"k\", linestyle=\"--\", alpha=0.5)\n", + " ax.plot([-0.7, 7.3], [7.3, -0.7], color=\"k\", linestyle=\"--\", alpha=0.5)\n", + "\n", + " # Draw red circles around the points that are misclassified (i.e. the points that are in the wrong bin)\n", + " for i, (X, y) in enumerate(zip([X_train, X_train], [y_train_idx, noisy_labels_idx])):\n", + " for j, (k, v) in enumerate(BINS_MAP.items()):\n", + " label_err = ax.scatter(\n", + " X[(y == v) & (y != y_train_idx), 0],\n", + " X[(y == v) & (y != y_train_idx), 1],\n", + " s=180,\n", + " marker=\"o\",\n", + " facecolor=\"none\",\n", + " edgecolors=\"red\",\n", + " linewidths=2.5,\n", + " alpha=0.5,\n", + " label=\"Label error\",\n", + " )\n", + "\n", + "\n", + " outlier = ax.scatter(X_out[:, 0], X_out[:, 1], color=\"k\", marker=\"x\", s=100, linewidth=2, label=\"Outlier\")\n", + "\n", + " # Plot the exact duplicate\n", + " dups = ax.scatter(\n", + " X_duplicate[:, 0],\n", + " X_duplicate[:, 1],\n", + " color=\"c\",\n", + " marker=\"x\",\n", + " s=100,\n", + " linewidth=2,\n", + " label=\"Duplicates\",\n", + " )\n", + " \n", + " first_legend = ax.legend(handles=[low, mid, high], loc=[0.75, 0.7], title=\"Given Class Label\", alignment=\"left\", title_fontproperties={\"weight\":\"semibold\"})\n", + " second_legend = ax.legend(handles=[label_err, outlier, dups], loc=[0.75, 0.45], title=\"Type of Issue\", alignment=\"left\", title_fontproperties={\"weight\":\"semibold\"})\n", + " \n", + " ax = plt.gca().add_artist(first_legend)\n", + " ax = plt.gca().add_artist(second_legend)\n", + " plt.tight_layout()\n", + "```\n", + " \n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "def plot_data(X_train, y_train_idx, noisy_labels_idx, X_out, X_duplicate):\n", + " # Plot data with clean labels and noisy labels, use BINS_MAP for the legend\n", + " fig, ax = plt.subplots(figsize=(6, 4))\n", + " \n", + " low = ax.scatter(X_train[noisy_labels_idx == 0, 0], X_train[noisy_labels_idx == 0, 1], label=\"low\")\n", + " mid = ax.scatter(X_train[noisy_labels_idx == 1, 0], X_train[noisy_labels_idx == 1, 1], label=\"mid\")\n", + " high = ax.scatter(X_train[noisy_labels_idx == 2, 0], X_train[noisy_labels_idx == 2, 1], label=\"high\")\n", + " \n", + " ax.set_title(\"Noisy labels\")\n", + " ax.set_xlabel(r\"$x_1$\", fontsize=16)\n", + " ax.set_ylabel(r\"$x_2$\", fontsize=16)\n", + "\n", + " # Plot true boundaries (x+y=3.3, x+y=6.6)\n", + " ax.set_xlim(-2.5, 8.5)\n", + " ax.set_ylim(-3.5, 9.0)\n", + " ax.plot([-0.7, 4.0], [4.0, -0.7], color=\"k\", linestyle=\"--\", alpha=0.5)\n", + " ax.plot([-0.7, 7.3], [7.3, -0.7], color=\"k\", linestyle=\"--\", alpha=0.5)\n", + "\n", + " # Draw red circles around the points that are misclassified (i.e. the points that are in the wrong bin)\n", + " for i, (X, y) in enumerate(zip([X_train, X_train], [y_train_idx, noisy_labels_idx])):\n", + " for j, (k, v) in enumerate(BINS_MAP.items()):\n", + " label_err = ax.scatter(\n", + " X[(y == v) & (y != y_train_idx), 0],\n", + " X[(y == v) & (y != y_train_idx), 1],\n", + " s=180,\n", + " marker=\"o\",\n", + " facecolor=\"none\",\n", + " edgecolors=\"red\",\n", + " linewidths=2.5,\n", + " alpha=0.5,\n", + " label=\"Label error\",\n", + " )\n", + "\n", + "\n", + " outlier = ax.scatter(X_out[:, 0], X_out[:, 1], color=\"k\", marker=\"x\", s=100, linewidth=2, label=\"Outlier\")\n", + "\n", + " # Plot the exact duplicate\n", + " dups = ax.scatter(\n", + " X_duplicate[:, 0],\n", + " X_duplicate[:, 1],\n", + " color=\"c\",\n", + " marker=\"x\",\n", + " s=100,\n", + " linewidth=2,\n", + " label=\"Duplicates\",\n", + " )\n", + " \n", + " title_fontproperties = {\"weight\":\"semibold\", \"size\": 8}\n", + " first_legend = ax.legend(handles=[low, mid, high], loc=[0.76, 0.7], title=\"Given Class Label\", alignment=\"left\", title_fontproperties=title_fontproperties, fontsize=8, markerscale=0.5)\n", + " second_legend = ax.legend(handles=[label_err, outlier, dups], loc=[0.76, 0.46], title=\"Type of Issue\", alignment=\"left\", title_fontproperties=title_fontproperties, fontsize=8, markerscale=0.5)\n", + " \n", + " ax = plt.gca().add_artist(first_legend)\n", + " ax = plt.gca().add_artist(second_legend)\n", + " plt.tight_layout()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot_data(X_train, y_train_idx, noisy_labels_idx, X_out, X_duplicate)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In real-world scenarios, you won't know the true labels or the distribution of the features, so we won't use these in this tutorial, except for evaluation purposes.\n", + "\n", + "\n", + "\n", + "`Datalab` has several ways of loading the data.\n", + "In this case, we'll simply wrap the training features and noisy labels in a dictionary so that we can pass it to `Datalab`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = {\"X\": X_train, \"y\": noisy_labels}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Other supported data formats for `Datalab` include: [HuggingFace Datasets](https://huggingface.co/docs/datasets/index) and [pandas DataFrame](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html). `Datalab` works across most data modalities (image, text, tabular, audio, etc). It is intended to find issues that commonly occur in datasets for which you have trained a supervised ML model, regardless of the type of data.\n", + "\n", + "Currently, pandas DataFrames that contain categorical columns might cause some issues when instantiating the `Datalab` object, so it is recommended to ensure that your DataFrame does not contain any categorical columns, or use other data formats (eg. python dictionary, HuggingFace Datasets) to pass in your data." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Get out-of-sample predicted probabilities from a classifier" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To detect certain types of issues in classification data (e.g. label errors), `Datalab` relies on predicted class probabilities from a trained model. Ideally, the prediction for each example should be out-of-sample (to avoid overfitting), coming from a copy of the model that was not trained on this example. \n", + "\n", + "This tutorial uses a simple logistic regression model \n", + "and the `cross_val_predict()` function from scikit-learn to generate out-of-sample predicted class probabilities for every example in the training set. You can replace this with *any* other classifier model and train it with cross-validation to get out-of-sample predictions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model = LogisticRegression()\n", + "pred_probs = cross_val_predict(\n", + " estimator=model, X=data[\"X\"], y=data[\"y\"], cv=5, method=\"predict_proba\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Use Datalab to find issues in the dataset" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We create a `Datalab` object from the dataset, also providing the name of the label column in the dataset. Only instantiate one `Datalab` object per dataset, and note that only classification datasets are supported for now.\n", + "\n", + "All that is need to audit your data is to call `find_issues()`.\n", + "This method accepts various inputs like: predicted class probabilities, numeric feature representations of the data. The more information you provide here, the more thoroughly `Datalab` will audit your data! Note that `features` should be some numeric representation of each example, either obtained through preprocessing transformation of your raw data or embeddings from a (pre)trained model. In this case, our data is already entirely numeric so we just provide the features directly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lab = Datalab(data, label_name=\"y\")\n", + "lab.find_issues(pred_probs=pred_probs, features=data[\"X\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's review the results of this audit using `report()`.\n", + "This provides a high-level summary of each type of issue found in the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lab.report()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Learn more about the issues in your dataset" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Datalab detects all sorts of issues in a dataset and what to do with the findings will vary case-by-case. For automated improvement of a dataset via best practices to handle auto-detected issues, try [Cleanlab Studio](https://cleanlab.ai/?utm_source=internal&utm_medium=blog&utm_campaign=clostostudio).\n", + "\n", + "To conceptually understand how each type of issue is defined and what it means if detected in your data, check out the [Issue Type Descriptions](../../cleanlab/datalab/guide/issue_type_description.html) page. The [Datalab Issue Types](https://docs.cleanlab.ai/stable/cleanlab/datalab/guide/issue_type_description.html) page also lists additional types of issues that `Datalab.find_issues()` can detect, as well as optional parameters you can specify for greater control over how your data are checked.\n", + "\n", + "Datalab offers several methods to understand more details about a particular issue in your dataset.\n", + "The `get_issue_summary()` method fetches summary statistics regarding how severe each type of issue is overall across the whole dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lab.get_issue_summary()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also only request the summary for a particular type of issue." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lab.get_issue_summary(\"label\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `get_issues()` method returns information for each individual example in the dataset including: whether or not it is plagued by this issue, as well as a *quality score* quantifying how severe this issue appears to be for this particular example. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lab.get_issues().head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Similar to above, we can pass the type of issue as a argument to `get_issues()` to get the information for one particular issue.\n", + "\n", + "Each example receives a separate *quality score* (betweeen 0 to 1) for each issue type. Lower scores indicate more severe instances of the issue, so you can sort by these values to see the most concerning examples in your dataset for each type of issue. The quality scores are directly comparable between examples/datasets, but not across different issue types. Here we show an example of how to get the examples identified as having the most severe label issues." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "examples_w_issue = (\n", + " lab.get_issues(\"label\")\n", + " .query(\"is_label_issue\")\n", + " .sort_values(\"label_score\")\n", + ")\n", + "\n", + "examples_w_issue.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Looking at the labels for some of these top-ranked examples, we find their given label was indeed incorrect!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Get additional information \n", + "\n", + "Additional information (statistics, intermediate results, etc) related to a particular issue type can be accessed via `get_info(issue_name)`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "label_issues_info = lab.get_info(\"label\")\n", + "label_issues_info[\"classes_by_label_quality\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This portion of the info shows overall label quality summaries of all examples annotated as a particular class (e.g. the `Label Issues` column is the estimated number of examples labeled as this class that should actually have a different label).\n", + "To learn more about this, see the documentation for the [cleanlab.dataset.rank_classes_by_label_quality](../../cleanlab/dataset.html#cleanlab.dataset.rank_classes_by_label_quality)\n", + "method.\n", + "\n", + "You can view all sorts of information regarding your dataset using the `get_info()` method with no arguments passed. This is not printed here as it returns a huge dictionary but feel free to check it out yourself! Don't worry if you don't understand all of the miscellaneous information in this `info` dictionary, none of it is critical to diagnose the issues in your dataset. Understanding miscellaneous info may require reading the documentation of the miscellaneous cleanlab functions which computed it.\n", + "\n", + "#### Near duplicate issues \n", + "\n", + "Let's also inspect the examples flagged as (near) duplicates.\n", + "For each such example, the `near_duplicate_sets` column below indicates which other examples in the dataset are highly similar to it (this value is empty for examples not flagged as nearly duplicated). The `near_duplicate_score` quantifies how similar each example is to its nearest neighbor in the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lab.get_issues(\"near_duplicate\").query(\"is_near_duplicate_issue\").sort_values(\"near_duplicate_score\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`Datalab` makes it very easy to check your datasets for all sorts of issues that are important to deal with for training robust models. The inputs it uses to detect issues can come from *any* model you have trained (the better your model, the more accurate the issue detection will be).\n", + "\n", + "To learn more, check out this [examples notebook](https://github.com/cleanlab/examples/blob/master/datalab_image_classification/datalab.ipynb) and the [advanced Datalab tutorial](datalab_advanced.html)." + ] + } + ], + "metadata": { + "celltoolbar": "Edit Metadata", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.8" + }, + "vscode": { + "interpreter": { + "hash": "d4d1e4263499bec80672ea0156c357c1ee493ec2b1c70f0acce89fc37c4a6abe" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/source/tutorials/datalab/index.rst b/docs/source/tutorials/datalab/index.rst new file mode 100644 index 0000000000..841dcb75d9 --- /dev/null +++ b/docs/source/tutorials/datalab/index.rst @@ -0,0 +1,10 @@ +Datalab Tutorials +================= + +.. toctree:: + :maxdepth: 1 + + Detecting Common Data Issues with Datalab + Perform Advanced Data Auditing with Datalab + Text Classification + Tabular Classification diff --git a/docs/source/tutorials/datalab/tabular.ipynb b/docs/source/tutorials/datalab/tabular.ipynb new file mode 100644 index 0000000000..cb6c359936 --- /dev/null +++ b/docs/source/tutorials/datalab/tabular.ipynb @@ -0,0 +1,523 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Classification with Tabular Data using Scikit-Learn and Datalab\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this 5-minute quickstart tutorial, we use cleanlab with scikit-learn models to find potential label errors in a classification dataset with tabular (numeric/categorical) features. Tabular (or *structured*) data are typically organized in a row/column format and stored in a SQL database or file types like: CSV, Excel, or Parquet. Here we consider a Student Grades dataset, which contains over 900 individuals who have three exam grades and some optional notes, each being assigned a letter grade (their class label). cleanlab automatically identifies _hundreds_ of examples in this dataset that were mislabeled with the incorrect final grade selected. This tutorial will teach you how to use this package to detect incorrect information in your own tabular datasets.\n", + "\n", + "**Overview of what we'll do in this tutorial:**\n", + "\n", + "- Train a classifier model (here scikit-learn's HistGradientBoostingClassifier, although any model could be used) and use this classifier to compute (out-of-sample) predicted class probabilities via cross-validation.\n", + "\n", + "- Create a K nearest neighbours (KNN) graph between the examples in the dataset.\n", + "\n", + "- Identify issues in the dataset with cleanlab's `Datalab` audit applied to the predictions and KNN graph.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "Quickstart\n", + "
\n", + " \n", + "Already have (out-of-sample) `pred_probs` from a model trained on your original data labels? Have a `knn_graph` computed between dataset examples (reflecting similarity in their feature values)? Run the code below to find issues in your dataset.\n", + "\n", + "
\n", + " \n", + "```ipython3 \n", + "from cleanlab import Datalab\n", + "\n", + "lab = Datalab(data=your_dataset, label_name=\"column_name_of_labels\")\n", + "lab.find_issues(pred_probs=your_pred_probs, knn_graph=knn_graph)\n", + "\n", + "lab.get_issues()\n", + "```\n", + " \n", + "
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Install required dependencies\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can use `pip` to install all packages required for this tutorial as follows:\n", + "\n", + "```ipython3\n", + "!pip install sklearn datasets\n", + "!pip install \"cleanlab[datalab]\"\n", + "# Make sure to install the version corresponding to this tutorial\n", + "# E.g. if viewing master branch documentation:\n", + "# !pip install git+https://github.com/cleanlab/cleanlab.git\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Package installation (hidden on docs website).\n", + "dependencies = [\"cleanlab\", \"sklearn\", \"datasets\"]\n", + "\n", + "if \"google.colab\" in str(get_ipython()): # Check if it's running in Google Colab\n", + " %pip install cleanlab # for colab\n", + " cmd = ' '.join([dep for dep in dependencies if dep != \"cleanlab\"])\n", + " %pip install $cmd\n", + "else:\n", + " missing_dependencies = []\n", + " for dependency in dependencies:\n", + " try:\n", + " __import__(dependency)\n", + " except ImportError:\n", + " missing_dependencies.append(dependency)\n", + "\n", + " if len(missing_dependencies) > 0:\n", + " print(\"Missing required dependencies:\")\n", + " print(*missing_dependencies, sep=\", \")\n", + " print(\"\\nPlease install them before running the rest of this notebook.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "from sklearn.model_selection import cross_val_predict\n", + "from sklearn.preprocessing import StandardScaler\n", + "from sklearn.ensemble import HistGradientBoostingClassifier\n", + "from sklearn.neighbors import NearestNeighbors\n", + "\n", + "from cleanlab import Datalab\n", + "\n", + "SEED = 100 # for reproducibility\n", + "np.random.seed(SEED)\n", + "random.seed(SEED)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Load and process the data\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We first load the data features and labels (which are possibly noisy).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "grades_data = pd.read_csv(\"https://s.cleanlab.ai/grades-tabular-demo-v2.csv\")\n", + "grades_data.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "X_raw = grades_data[[\"exam_1\", \"exam_2\", \"exam_3\", \"notes\"]]\n", + "labels = grades_data[\"letter_grade\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next we preprocess the data. Here we apply one-hot encoding to columns with categorical values and standardize the values in numeric columns." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cat_features = [\"notes\"]\n", + "X_encoded = pd.get_dummies(X_raw, columns=cat_features, drop_first=True)\n", + "\n", + "numeric_features = [\"exam_1\", \"exam_2\", \"exam_3\"]\n", + "scaler = StandardScaler()\n", + "X_processed = X_encoded.copy()\n", + "X_processed[numeric_features] = scaler.fit_transform(X_encoded[numeric_features])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "Bringing Your Own Data (BYOD)?\n", + "\n", + "Assign your data's features to variable `X` and its labels to variable `labels` instead.\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Select a classification model and compute out-of-sample predicted probabilities\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we use a simple histogram-based gradient boosting model (similar to XGBoost), but you can choose any suitable scikit-learn model for this tutorial.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "clf = HistGradientBoostingClassifier()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To find potential labeling errors, cleanlab requires a probabilistic prediction from your model for every datapoint. However, these predictions will be _overfitted_ (and thus unreliable) for examples the model was previously trained on. For the best results, cleanlab should be applied with **out-of-sample** predicted class probabilities, i.e., on examples held out from the model during the training.\n", + "\n", + "K-fold cross-validation is a straightforward way to produce out-of-sample predicted probabilities for every datapoint in the dataset by training K copies of our model on different data subsets and using each copy to predict on the subset of data it did not see during training. We can implement this via the `cross_val_predict` method from scikit-learn:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "num_crossval_folds = 5 \n", + "pred_probs = cross_val_predict(\n", + " clf,\n", + " X_processed,\n", + " labels,\n", + " cv=num_crossval_folds,\n", + " method=\"predict_proba\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Construct K nearest neighbours graph" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The KNN graph reflects how close each example is when compared to other examples in our dataset (in the numerical space of preprocessed feature values). This similarity information is used by Datalab to identify issues like outliers in our data. For tabular data, think carefully about the most appropriate way to define the similarity between two examples.\n", + "\n", + "Here we use the `NearestNeighbors` class in sklearn to easily compute this graph (with similarity defined by the Euclidean distance between feature values). The graph should be represented as a sparse matrix with nonzero entries indicating nearest neighbors of each example and their distance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "KNN = NearestNeighbors(metric='euclidean')\n", + "KNN.fit(X_processed.values)\n", + "\n", + "knn_graph = KNN.kneighbors_graph(mode=\"distance\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Use cleanlab to find label issues\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Based on the given labels, predicted probabilities, and KNN graph, cleanlab can quickly help us identify suspicious values in our grades table.\n", + "\n", + "We use cleanlab's `Datalab` class which has several ways of loading the data. In this case, we’ll simply wrap the dataset (features and noisy labels) in a dictionary that is used instantiate a `Datalab` object such that it can audit our dataset for various types of issues." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = {\"X\": X_processed.values, \"y\": labels}\n", + "\n", + "lab = Datalab(data, label_name=\"y\")\n", + "lab.find_issues(pred_probs=pred_probs, knn_graph=knn_graph)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "lab.report()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Label issues\n", + "\n", + "The above report shows that cleanlab identified many label issues in the data. We can see which examples are estimated to be mislabeled (as well as a numeric quality score quantifying how likely their label is correct) via the `get_issues` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "issue_results = lab.get_issues(\"label\")\n", + "issue_results.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To review the most severe label issues, sort the DataFrame above by the `label_score` column (a lower score represents that the label is less likely to be correct). \n", + "\n", + "Let's review some of the most likely label errors:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sorted_issues = issue_results.sort_values(\"label_score\").index\n", + "\n", + "X_raw.iloc[sorted_issues].assign(\n", + " given_label=labels.iloc[sorted_issues], \n", + " predicted_label=issue_results[\"predicted_label\"].iloc[sorted_issues]\n", + ").head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The dataframe above shows the original label (`given_label`) for examples that cleanlab finds most likely to be mislabeled, as well as an alternative `predicted_label` for each example.\n", + "\n", + "These examples have been labeled incorrectly and should be carefully re-examined - a student with grades of 89, 95 and 73 surely does not deserve a D! " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Outlier issues\n", + "\n", + "According to the report, our dataset contains some outliers. We can see which examples are outliers (and a numeric quality score quantifying how typical each example appears to be) via `get_issues`. We sort the resulting DataFrame by cleanlab's outlier quality score to see the most severe outliers in our dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "outlier_results = lab.get_issues(\"outlier\")\n", + "sorted_outliers= outlier_results.sort_values(\"outlier_score\").index\n", + "\n", + "X_raw.iloc[sorted_outliers].head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The student at index 3 has fractional exam scores, which is likely a error. We also see that the students at index 0 and 4 have numerical values in their notes section, which is also probably unintended. Lastly, we see that the student at index 8 has a html string in their notes section, definitely a mistake!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Near-duplicate issues\n", + "\n", + "According to the report, our dataset contains some sets of nearly duplicated examples.\n", + "We can see which examples are (nearly) duplicated (and a numeric quality score quantifying how dissimilar each example is from its nearest neighbor in the dataset) via `get_issues`. We sort the resulting DataFrame by cleanlab's near-duplicate quality score to see the examples in our dataset that are most nearly duplicated." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "duplicate_results = lab.get_issues(\"near_duplicate\")\n", + "duplicate_results.sort_values(\"near_duplicate_score\").head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "duplicate_results.iloc[[691, 294, 251, 820, 845]]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The results above show which examples cleanlab considers nearly duplicated (rows where is_near_duplicate_issue == True). Here, we see many examples that cleanlab has flagged as being nearly duplicated. Let's view these examples to see how similar they are, starting with the top one.\n", + "We compare this example (student 690) against the example cleanlab has identified in the `near_duplicate_sets` (student 246)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "X_raw.iloc[[690, 246]]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These examples are exact duplicates! Perhaps the same information was accidentally recorded twice in this data." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For example 691, cleanlab identified four examples (294, 251, 820 and 845) that are near duplicates of it, let's check them out:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "X_raw.iloc[[691, 294, 251, 820, 845]]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These students are indeed very similar to one another! Including near/exact duplicates in a dataset may have unintended effects on models; be wary about splitting them across training/test sets." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This tutorial highlighted a straightforward approach to detect potentially incorrect information in any tabular dataset. Just use Datalab with any ML model -- the better the model, the more accurate the data errors detected by Datalab will be!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Note: This cell is only for docs.cleanlab.ai, if running on local Jupyter or Colab, please ignore it.\n", + "\n", + "identified_label_issues = issue_results[issue_results[\"is_label_issue\"] == True]\n", + "label_issue_indices = [3, 723, 709, 886, 689] # check these examples were found in label issues\n", + "if not all(x in identified_label_issues.index for x in label_issue_indices):\n", + " raise Exception(\"Some highlighted examples are missing from identified_label_issues.\")\n", + " \n", + "identified_outlier_issues = outlier_results[outlier_results[\"is_outlier_issue\"] == True]\n", + "outlier_issue_indices = [3, 7, 0, 4, 8] # check these examples were found in outlier issues\n", + "if not all(x in identified_outlier_issues.index for x in outlier_issue_indices):\n", + " raise Exception(\"Some highlighted examples are missing from identified_outlier_issues.\")\n", + " \n", + "identified_duplicate_issues = duplicate_results[duplicate_results[\"is_near_duplicate_issue\"] == True]\n", + "duplicate_issue_indices = [690, 246, 691, 294] # check these examples were found in duplicate issues\n", + "if not all(x in identified_duplicate_issues.index for x in duplicate_issue_indices):\n", + " raise Exception(\"Some highlighted examples are missing from identified_duplicate_issues.\")\n", + " \n", + "# check that the near duplicates shown are actually flagged as near duplicate sets\n", + "if not set(duplicate_results.iloc[691][\"near_duplicate_sets\"]) == {294, 251, 820, 845}:\n", + " raise Exception(\"These examples are not in the same near duplicate set\")" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "cda20062bc42cfdcaa0f9720c0b28e880bba110e9dfce6c1689934eec9b595a1" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.8" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/source/tutorials/datalab/text.ipynb b/docs/source/tutorials/datalab/text.ipynb new file mode 100644 index 0000000000..dd564b7dc4 --- /dev/null +++ b/docs/source/tutorials/datalab/text.ipynb @@ -0,0 +1,567 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Text Classification with Transformers and Datalab\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this 5-minute quickstart tutorial, we use cleanlab to find potential label errors in an intent classification dataset composed of (text) customer service requests at an online bank. We consider a subset of the [Banking77-OOS Dataset](https://arxiv.org/abs/2106.04564) containing 1,000 customer service requests which can be classified into 10 categories corresponding to the intent of the request. Cleanlab automatically identifies bad examples in our dataset, including mislabeled data, out-of-scope examples (outliers), or otherwise ambiguous examples. Consider filtering or correcting such bad examples before you dive deep into modeling your data!\n", + "\n", + "**Overview of what we'll do in this tutorial:**\n", + "\n", + "- Use a pretrained transformer model to extract the text embeddings from the customer service requests\n", + "\n", + "- Train a simple Logistic Regression model on the text embeddings to compute out-of-sample predicted probabilities\n", + "\n", + "- Run cleanlab's `Datalab` audit with these predictions and embeddings in order to identify problems like: label issues, outliers, and near duplicates in the dataset." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "Quickstart\n", + "
\n", + " \n", + "Already have (out-of-sample) `pred_probs` from a model trained on an existing set of labels? Maybe you have some numeric `features` as well? Run the code below to find any potential label errors in your dataset.\n", + "\n", + "
\n", + " \n", + "```ipython3 \n", + "from cleanlab import Datalab\n", + "\n", + "lab = Datalab(data=your_dataset, label_name=\"column_name_of_labels\")\n", + "lab.find_issues(pred_probs=your_pred_probs, features=your_features)\n", + "\n", + "lab.report()\n", + "lab.get_issues()\n", + "```\n", + " \n", + "
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Install required dependencies\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can use `pip` to install all packages required for this tutorial as follows:\n", + "\n", + "```ipython3\n", + "!pip install sklearn sentence-transformers\n", + "!pip install \"cleanlab[datalab]\"\n", + "# Make sure to install the version corresponding to this tutorial\n", + "# E.g. if viewing master branch documentation:\n", + "# !pip install git+https://github.com/cleanlab/cleanlab.git\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Package installation (hidden on docs.cleanlab.ai).\n", + "# If running on Colab, may want to use GPU (select: Runtime > Change runtime type > Hardware accelerator > GPU)\n", + "# Package versions we used:scikit-learn==1.2.0 sentence-transformers==2.2.2\n", + "\n", + "dependencies = [\"cleanlab\", \"sklearn\", \"sentence_transformers\", \"datasets\"]\n", + "\n", + "# Supress outputs that may appear if tensorflow happens to be improperly installed: \n", + "import os \n", + "\n", + "os.environ[\"TOKENIZERS_PARALLELISM\"] = \"false\" # disable parallelism to avoid deadlocks with huggingface\n", + "\n", + "if \"google.colab\" in str(get_ipython()): # Check if it's running in Google Colab\n", + " %pip install cleanlab # for colab\n", + " cmd = ' '.join([dep for dep in dependencies if dep != \"cleanlab\"])\n", + " %pip install $cmd\n", + "else:\n", + " missing_dependencies = []\n", + " for dependency in dependencies:\n", + " try:\n", + " __import__(dependency)\n", + " except ImportError:\n", + " missing_dependencies.append(dependency)\n", + "\n", + " if len(missing_dependencies) > 0:\n", + " print(\"Missing required dependencies:\")\n", + " print(*missing_dependencies, sep=\", \")\n", + " print(\"\\nPlease install them before running the rest of this notebook.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import re \n", + "import string \n", + "import pandas as pd \n", + "from sklearn.metrics import accuracy_score, log_loss \n", + "from sklearn.model_selection import cross_val_predict \n", + "from sklearn.linear_model import LogisticRegression\n", + "from sentence_transformers import SentenceTransformer\n", + "\n", + "from cleanlab import Datalab" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# This cell is hidden from docs.cleanlab.ai \n", + "\n", + "import random \n", + "import numpy as np \n", + "\n", + "pd.set_option(\"display.max_colwidth\", None) \n", + "\n", + "SEED = 123456 # for reproducibility\n", + "np.random.seed(SEED)\n", + "random.seed(SEED)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Load and format the text dataset\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = pd.read_csv(\"https://s.cleanlab.ai/banking-intent-classification.csv\")\n", + "data.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "raw_texts, labels = data[\"text\"].values, data[\"label\"].values\n", + "num_classes = len(set(labels))\n", + "\n", + "print(f\"This dataset has {num_classes} classes.\")\n", + "print(f\"Classes: {set(labels)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's view the i-th example in the dataset:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "i = 1 # change this to view other examples from the dataset\n", + "print(f\"Example Label: {labels[i]}\")\n", + "print(f\"Example Text: {raw_texts[i]}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The data is stored as two numpy arrays:\n", + "\n", + "1. `raw_texts` stores the customer service requests utterances in text format\n", + "2. `labels` stores the intent categories (labels) for each example" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "Bringing Your Own Data (BYOD)?\n", + "\n", + "You can easily replace the above with your own text dataset, and continue with the rest of the tutorial.\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next we convert the text strings into vectors better suited as inputs for our ML models. \n", + "\n", + "We will use numeric representations from a pretrained Transformer model as embeddings of our text. The [Sentence Transformers](https://huggingface.co/docs/hub/sentence-transformers) library offers simple methods to compute these embeddings for text data. Here, we load the pretrained `electra-small-discriminator` model, and then run our data through network to extract a vector embedding of each example." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "transformer = SentenceTransformer('google/electra-small-discriminator')\n", + "text_embeddings = transformer.encode(raw_texts)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Our subsequent ML model will directly operate on elements of `text_embeddings` in order to classify the customer service requests." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Define a classification model and compute out-of-sample predicted probabilities" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A typical way to leverage pretrained networks for a particular classification task is to add a linear output layer and fine-tune the network parameters on the new data. However this can be computationally intensive. Alternatively, we can freeze the pretrained weights of the network and only train the output layer without having to rely on GPU(s). Here we do this conveniently by fitting a scikit-learn linear model on top of the extracted embeddings.\n", + "\n", + "To identify label issues, cleanlab requires a probabilistic prediction from your model for each datapoint. However these predictions will be _overfit_ (and thus unreliable) for datapoints the model was previously trained on. cleanlab is intended to only be used with **out-of-sample** predicted class probabilities, i.e. on datapoints held-out from the model during the training.\n", + "\n", + "Here we obtain out-of-sample predicted class probabilities for every example in our dataset using a Logistic Regression model with cross-validation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "model = LogisticRegression(max_iter=400)\n", + "\n", + "pred_probs = cross_val_predict(model, text_embeddings, labels, method=\"predict_proba\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Use cleanlab to find issues in your dataset" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Given feature embeddings and the (out-of-sample) predicted class probabilities obtained from any model you have, cleanlab can quickly help you identify low-quality examples in your dataset.\n", + "\n", + "Here, we use cleanlab's `Datalab` to find issues in our data. Datalab offers several ways of loading the data; we’ll simply wrap the training features and noisy labels in a dictionary. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data_dict = {\"texts\": raw_texts, \"labels\": labels}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All that is need to audit your data is to call `find_issues()`. We pass in the predicted probabilities and the feature embeddings obtained above, but you do not necessarily need to provide all of this information depending on which types of issues you are interested in. The more inputs you provide, the more types of issues `Datalab` can detect in your data. Using a better model to produce these inputs will ensure cleanlab more accurately estimates issues." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "lab = Datalab(data_dict, label_name=\"labels\")\n", + "lab.find_issues(pred_probs=pred_probs, features=text_embeddings)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After the audit is complete, review the findings using the `report` method:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "lab.report()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Label issues\n", + "\n", + "The report indicates that cleanlab identified many label issues in our dataset. We can see which examples are flagged as likely mislabeled and the label quality score for each example using the `get_issues` method, specifying `label` as an argument to focus on label issues in the data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "label_issues = lab.get_issues(\"label\")\n", + "label_issues.head() " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This method returns a dataframe containing a label quality score for each example. These numeric scores lie between 0 and 1, where lower scores indicate examples more likely to be mislabeled. The dataframe also contains a boolean column specifying whether or not each example is identified to have a label issue (indicating it is likely mislabeled)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can get the subset of examples flagged with label issues, and also sort by label quality score to find the indices of the 5 most likely mislabeled examples in our dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "identified_label_issues = label_issues[label_issues[\"is_label_issue\"] == True]\n", + "lowest_quality_labels = label_issues[\"label_score\"].argsort()[:5].to_numpy()\n", + "\n", + "print(\n", + " f\"cleanlab found {len(identified_label_issues)} potential label errors in the dataset.\\n\"\n", + " f\"Here are indices of the top 5 most likely errors: \\n {lowest_quality_labels}\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's review some of the most likely label errors. \n", + "\n", + "Here we display the top 5 examples identified as the most likely label errors in the dataset, together with their given (original) label and a suggested alternative label from cleanlab.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data_with_suggested_labels = pd.DataFrame(\n", + " {\"text\": raw_texts, \"given_label\": labels, \"suggested_label\": label_issues[\"predicted_label\"]}\n", + ")\n", + "data_with_suggested_labels.iloc[lowest_quality_labels]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "scrolled": true + }, + "source": [ + "These are very clear label errors that cleanlab has identified in this data! Note that the `given_label` does not correctly reflect the intent of these requests, whoever produced this dataset made many mistakes that are important to address before modeling the data." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Outlier issues\n", + "\n", + "According to the report, our dataset contains some outliers.\n", + "We can see which examples are outliers (and a numeric quality score quantifying how typical each example appears to be) via `get_issues`. We sort the resulting DataFrame by cleanlab's outlier quality score to see the most severe outliers in our dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "outlier_issues = lab.get_issues(\"outlier\")\n", + "outlier_issues.sort_values(\"outlier_score\").head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lowest_quality_outliers = outlier_issues[\"outlier_score\"].argsort()[:5]\n", + "\n", + "data.iloc[lowest_quality_outliers]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We see that cleanlab has identified entries in this dataset that do not appear to be proper customer requests. Outliers in this dataset appear to be out-of-scope customer requests and other nonsensical text which does not make sense for intent classification. Carefully consider whether such outliers may detrimentally affect your data modeling, and consider removing them from the dataset if so." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Near-duplicate issues\n", + "\n", + "According to the report, our dataset contains some sets of nearly duplicated examples.\n", + "We can see which examples are (nearly) duplicated (and a numeric quality score quantifying how dissimilar each example is from its nearest neighbor in the dataset) via `get_issues`. We sort the resulting DataFrame by cleanlab's near-duplicate quality score to see the text examples in our dataset that are most nearly duplicated." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "duplicate_issues = lab.get_issues(\"near_duplicate\")\n", + "duplicate_issues.sort_values(\"near_duplicate_score\").head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The results above show which examples cleanlab considers nearly duplicated (rows where `is_near_duplicate_issue == True`). Here, we see that example 160 and 148 are nearly duplicated, as are example 546 and 514.\n", + "\n", + "Let's view these examples to see how similar they are." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data.iloc[[160, 148]]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data.iloc[[546, 514]]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We see that these two sets of request are indeed very similar to one another! Including near duplicates in a dataset may have unintended effects on models, and be wary about splitting them across training/test sets." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As demonstrated above, cleanlab can automatically shortlist the most likely issues in your dataset to help you better curate your dataset for subsequent modeling. With this shortlist, you can decide whether to fix these label issues or remove nonsensical or duplicated examples from your dataset to obtain a higher-quality dataset for training your next ML model. cleanlab's issue detection can be run with outputs from *any* type of model you initially trained.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Note: This cell is only for docs.cleanlab.ai, if running on local Jupyter or Colab, please ignore it.\n", + "\n", + "label_issue_indices = [981, 974, 982] # check these examples were found in label issues\n", + "if not all(x in identified_label_issues.index for x in label_issue_indices):\n", + " raise Exception(\"Some highlighted examples are missing from identified_label_issues.\")\n", + " \n", + "identified_outlier_issues = outlier_issues[outlier_issues[\"is_outlier_issue\"] == True]\n", + "outlier_issue_indices = [994, 989, 999] # check these examples were found in duplicates\n", + "if not all(x in identified_outlier_issues.index for x in outlier_issue_indices):\n", + " raise Exception(\"Some highlighted examples are missing from identified_outlier_issues.\")\n", + "\n", + "identified_duplicate_issues = duplicate_issues[duplicate_issues[\"is_near_duplicate_issue\"] == True]\n", + "duplicate_issue_indices = [160, 148, 546, 514] # check these examples were found in duplicates\n", + "if not all(x in identified_duplicate_issues.index for x in duplicate_issue_indices):\n", + " raise Exception(\"Some highlighted examples are missing from identified_duplicate_issues.\")" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "Text x TensorFlow", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.8" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/docs/source/tutorials/faq.ipynb b/docs/source/tutorials/faq.ipynb index fdd9ddc04a..fc8c93fa16 100644 --- a/docs/source/tutorials/faq.ipynb +++ b/docs/source/tutorials/faq.ipynb @@ -7,7 +7,9 @@ "source": [ "# FAQ\n", "\n", - "Answers to frequently asked questions about the [cleanlab](https://github.com/cleanlab/cleanlab) open-source package." + "Answers to frequently asked questions about the [cleanlab](https://github.com/cleanlab/cleanlab) open-source package.\n", + "\n", + "The code snippets in this FAQ come from a fully executable notebook you can run via Colab or locally by downloading it [here](https://github.com/cleanlab/cleanlab/blob/master/docs/source/tutorials/faq.ipynb).\n" ] }, { @@ -19,7 +21,7 @@ }, "outputs": [], "source": [ - "# This cell is for internal CI purposes. Run it to ensure all other cells below can be executed in your own notebook\n", + "# This cell is hidden on docs.cleanlab.ai. Execute it to ensure all other cells below can be executed in your own notebook\n", "\n", "import os \n", "import logging \n", @@ -38,7 +40,10 @@ "pred_probs[np.arange(N),labels] += 4 # make pred_probs accurate\n", "pred_probs = pred_probs/pred_probs.sum(axis=1)[:, np.newaxis]\n", "data = np.array([[label+np.random.uniform(), label+np.random.uniform()] for label in labels])\n", - "labels[-num_errors:] = 0 # introduce label errors\n", + "# introduce label errors in last few examples:\n", + "og0_indices = labels[-num_errors:] == 0\n", + "labels[-num_errors:] = 0\n", + "labels[-num_errors:][og0_indices] = 1\n", "\n", "your_classifier=sklearn.linear_model.LogisticRegression() # toy classifier" ] @@ -147,7 +152,7 @@ "id": "b386dfc8", "metadata": {}, "source": [ - "If you have already found issues via:" + "Otherwise if you have already found issues via:" ] }, { @@ -165,7 +170,7 @@ "id": "ad9ca03e", "metadata": {}, "source": [ - "then you can see your trained classifier's class prediction for each flagged example via: " + "then you can see your trained classifier's class prediction for each flagged example like this: " ] }, { @@ -183,7 +188,7 @@ "id": "a668b74b", "metadata": {}, "source": [ - "where you can see the classifier's class prediction for every example via:" + "Here you can see the classifier's class prediction for every example via:" ] }, { @@ -202,7 +207,193 @@ "metadata": {}, "source": [ "We caution against just blindly taking the predicted label for granted, many of these suggestions may be wrong! \n", - "You will be able to produce a much better version of your dataset interactively using [Cleanlab Studio](https://cleanlab.ai/studio/), which helps you efficiently fix issues like this in large datasets." + "You will be able to produce a much better version of your dataset interactively using [Cleanlab Studio](https://cleanlab.ai/studio/?utm_source=github&utm_medium=docs&utm_campaign=clostostudio), which helps you efficiently fix issues like this in large datasets." + ] + }, + { + "cell_type": "markdown", + "id": "bcc97591", + "metadata": {}, + "source": [ + "### How should I handle label errors in train vs. test data?\n", + "\n", + "If you do not address label errors in your test data, you may not even know when you have produced a better ML model because the evaluation is too noisy. For the best-trained models and most reliable evaluation of them, you should fix label errors in both training and testing data.\n", + "\n", + "To do this efficiently, first use cleanlab to automatically find label issues in both sets. You can simply merge these two sets into one larger dataset and run cross-validation training + `find_label_issues()` on the merged datataset. Calling the [CleanLearning.find_label_issues()](../cleanlab/classification.html) method on your merged dataset does both these steps for you with any scikit-learn compatible classifier you choose.\n", + "\n", + "After finding label issues, be **wary** about auto-correcting the labels for test examples (as cautioned against above). Instead manually fix the labels for your test data via careful review of the flagged issues. You can use [Cleanlab Studio](https://cleanlab.ai/studio/) to fix labels efficiently.\n", + "\n", + "Auto-correcting labels for your training data is fair game, which should improve ML performance (if properly evaluated with clean test labels). You can boost ML performance further by manually fixing the training examples flagged with label issues, as demonstrated in this article:\n", + "\n", + "[**Handling Mislabeled Tabular Data to Improve Your XGBoost Model**](https://cleanlab.ai/blog/label-errors-tabular-datasets/)" + ] + }, + { + "cell_type": "markdown", + "id": "21f42f24", + "metadata": {}, + "source": [ + "### How can I find label issues in big datasets with limited memory? " + ] + }, + { + "cell_type": "markdown", + "id": "089f505e", + "metadata": {}, + "source": [ + "For a dataset with many rows and/or classes, there are more efficient methods in the `label_issues_batched` module. These methods read data in mini-batches and you can reduce the `batch_size` to control how much memory they require. Below is an example of how to use the `find_label_issues_batched()` method from this module, which can load mini-batches of data from `labels`, `pred_probs` saved as .npy files on disk. You can also run this method on Zarr arrays loaded from .zarr files. Try playing with the `n_jobs` argument for further multiprocessing speedups. If you need greater flexibility, check out the `LabelInspector` class from this module." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41714b51", + "metadata": {}, + "outputs": [], + "source": [ + "# We'll assume your big arrays of labels, pred_probs have been saved to file like this:\n", + "from tempfile import mkdtemp\n", + "import os.path as path\n", + "\n", + "labels_file = path.join(mkdtemp(), \"labels.npy\")\n", + "pred_probs_file = path.join(mkdtemp(), \"pred_probs.npy\")\n", + "np.save(labels_file, labels)\n", + "np.save(pred_probs_file, pred_probs)\n", + "\n", + "# Code to find label issues by loading data from file in batches:\n", + "from cleanlab.experimental.label_issues_batched import find_label_issues_batched\n", + "\n", + "batch_size = 10000 # for efficiency, set this to as large of a value as your memory can handle\n", + "\n", + "# Indices of examples with label issues, sorted by label quality score (most severe to least severe):\n", + "indices_of_examples_with_issues = find_label_issues_batched(\n", + " labels_file=labels_file, pred_probs_file=pred_probs_file, batch_size=batch_size\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20476c70", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# This cell is hidden on docs.cleanlab.ai, and is only for internal testing. You can ignore it.\n", + "\n", + "issue_indices = cleanlab.filter.find_label_issues(labels, pred_probs, filter_by = \"low_self_confidence\", return_indices_ranked_by=\"self_confidence\")\n", + "assert np.abs(len(issue_indices) - len(indices_of_examples_with_issues)) < 2, \"num issues differ in batched mode\"\n", + "set1 = set(issue_indices)\n", + "set2 = set(indices_of_examples_with_issues)\n", + "intersection = len(list(set1.intersection(set2)))\n", + "union = len(set1) + len(set2) - intersection\n", + "assert float(intersection) / union > 0.95, \"issue indices differ in batched mode\"" + ] + }, + { + "cell_type": "markdown", + "id": "438b424d", + "metadata": {}, + "source": [ + "**To use less memory and get results faster if your dataset has many classes:** Try merging the rare classes into a single \"Other\" class before you find label issues. The resulting issues won't be affected much since cleanlab anyway does not have enough data to accurately diagnose label errors in classes that are rarely seen. To do this, you should aggregate all the probability assigned to the rare classes in `pred_probs` into a single new dimension of `pred_probs_merged` (where this new array no longer has columns for the rare classes). Here is a function that does this for you, which you can also modify as needed:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6983cdad", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# This cell is hidden on docs.cleanlab.ai\n", + "# Add two rare additional classes to the dataset:\n", + "\n", + "num_rare_instances = 3\n", + "small_prob = 1e-4\n", + "pred_probs = np.hstack((pred_probs, np.ones((len(pred_probs),2))*small_prob))\n", + "pred_probs = pred_probs / np.sum(pred_probs, axis=1)[:, np.newaxis]\n", + "labels[:num_rare_instances] = 3\n", + "labels[num_rare_instances:(2*num_rare_instances)] = 4" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9092b8a0", + "metadata": {}, + "outputs": [], + "source": [ + "from cleanlab.internal.util import value_counts # use this to count how often each class occurs in labels\n", + "\n", + "def merge_rare_classes(labels, pred_probs, count_threshold = 10):\n", + " \"\"\" \n", + " Returns: labels, pred_probs after we merge all rare classes into a single 'Other' class.\n", + " Merged pred_probs has less columns. Rare classes are any occuring less than `count_threshold` times.\n", + " Also returns: `class_mapping_orig2new`, a dict to map new classes in merged labels back to classes \n", + " in original labels, useful for interpreting outputs from `dataset.heath_summary()` or `count.confident_joint()`.\n", + " \"\"\"\n", + " num_classes = pred_probs.shape[1]\n", + " num_examples_per_class = value_counts(labels, num_classes=num_classes)\n", + " rare_classes = [c for c in range(num_classes) if num_examples_per_class[c] < count_threshold]\n", + " if len(rare_classes) < 1:\n", + " raise ValueError(\"No rare classes found at the given `count_threshold`, merging is unnecessary unless you increase it.\")\n", + "\n", + " num_classes_merged = num_classes - len(rare_classes) + 1 # one extra class for all the merged ones\n", + " other_class = num_classes_merged - 1\n", + " labels_merged = labels.copy()\n", + " class_mapping_orig2new = {} # key = original class in `labels`, value = new class in `labels_merged`\n", + " new_c = 0\n", + " for c in range(num_classes):\n", + " if c in rare_classes:\n", + " class_mapping_orig2new[c] = other_class\n", + " else:\n", + " class_mapping_orig2new[c] = new_c\n", + " new_c += 1\n", + " labels_merged[labels == c] = class_mapping_orig2new[c]\n", + "\n", + " merged_prob = np.sum(pred_probs[:, rare_classes], axis=1, keepdims=True) # total probability over all merged classes for each example\n", + " pred_probs_merged = np.hstack((np.delete(pred_probs, rare_classes, axis=1), merged_prob)) # assumes new_class is as close to original_class in sorted order as is possible after removing the merged original classes\n", + " # check a few rows of probabilities after merging to verify they still sum to 1:\n", + " num_check = 1000 # only check a few rows for efficiency\n", + " ones_array_ref = np.ones(min(num_check,len(pred_probs)))\n", + " if np.isclose(np.sum(pred_probs[:num_check], axis=1), ones_array_ref).all() and (not np.isclose(np.sum(pred_probs_merged[:num_check], axis=1), ones_array_ref).all()):\n", + " raise ValueError(\"merged pred_probs do not sum to 1 in each row, check that merging was correctly done.\")\n", + " \n", + " return (labels_merged, pred_probs_merged, class_mapping_orig2new)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b0a01109", + "metadata": {}, + "outputs": [], + "source": [ + "from cleanlab.filter import find_label_issues # can alternatively use find_label_issues_batched() shown above\n", + "\n", + "labels_merged, pred_probs_merged, class_mapping_orig2new = merge_rare_classes(labels, pred_probs, count_threshold=5)\n", + "examples_w_issues = find_label_issues(labels_merged, pred_probs_merged, return_indices_ranked_by=\"self_confidence\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b1da032", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# This cell is hidden on docs.cleanlab.ai, and is only for internal testing. You can ignore it.\n", + "\n", + "rare_classes = [c for c in class_mapping_orig2new.keys() if class_mapping_orig2new[c] == pred_probs_merged.shape[1]-1]\n", + "og_examples_w_issues = find_label_issues(labels, pred_probs, return_indices_ranked_by=\"self_confidence\")\n", + "examples_of_interest = [x for x in examples_w_issues if labels[x] not in rare_classes]\n", + "og_examples_of_interest = [x for x in og_examples_w_issues if labels[x] not in rare_classes]\n", + "assert set(examples_of_interest) == set(og_examples_of_interest), \"merged label issues differ from non-merged label issues\"" ] }, { @@ -224,7 +415,7 @@ "You can still use cleanlab with other data formats though! Just separately obtain predicted probabilities (`pred_probs`) from your model via cross-validation and pass them as inputs. \n", "\n", "\n", - "If CleanLearning is running successfully but not improving predictive accuracy of your model, here are some tips:\n", + "If CleanLearning is running successfully but not improving predictive accuracy of your model, here are some tips:\n", "\n", "1. Use cleanlab to find label issues in your test data as well (we recommend pooling `labels` across both training and test data into one input for `find_label_issues()`). Then manually review and fix label issues identified in the test data to verify accuracy measurements are actually meaningful.\n", "\n", @@ -372,8 +563,8 @@ "id": "1a117547", "metadata": {}, "source": [ - "These questions are automatically handled for you in [Cleanlab Studio](https://cleanlab.ai/studio) -- our platform for no-code data improvement.\n", - "While this open-source library **finds** data issues, an interface is needed to efficiently **fix** these issues in your dataset. [Cleanlab Studio](https://cleanlab.ai/studio) is a no-code platform to find and fix problems in real-world ML datasets. Studio automatically runs optimized versions of the algorithms from this open-source library on top of AutoML models fit to your data, and presents detected issues in a smart data editing interface. Think of it like a data cleaning assistant that helps you quickly improve the quality of your data (via AI/automation + streamlined UX)." + "These questions are automatically handled for you in [Cleanlab Studio](https://cleanlab.ai/studio/?utm_source=github&utm_medium=docs&utm_campaign=clostostudio) -- our platform for no-code data improvement.\n", + "While this open-source library **finds** data issues, an interface is needed to efficiently **fix** these issues in your dataset. [Cleanlab Studio](https://cleanlab.ai/studio/?utm_source=github&utm_medium=docs&utm_campaign=clostostudio) is a no-code platform to find and fix problems in real-world ML datasets. Studio automatically runs optimized versions of the algorithms from this open-source library on top of AutoML models fit to your data, and presents detected issues in a smart data editing interface. Think of it like a data cleaning assistant that helps you quickly improve the quality of your data (via AI/automation + streamlined UX)." ] }, { @@ -426,7 +617,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.9.12" } }, "nbformat": 4, diff --git a/docs/source/tutorials/image.ipynb b/docs/source/tutorials/image.ipynb index 7f00bae63c..25a39d72a9 100644 --- a/docs/source/tutorials/image.ipynb +++ b/docs/source/tutorials/image.ipynb @@ -1,6 +1,7 @@ { "cells": [ { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -19,7 +20,7 @@ "\n", "- Use this model to compute out-of-sample predicted probabilities, `pred_probs`, via cross-validation.\n", "\n", - "- Compute a list of potential label errors with cleanlab's `find_label_issues` method.\n" + "- Use these predictions to estimate which images in the dataset are mislabeled via cleanlab's `Datalab` class.\n" ] }, { @@ -30,22 +31,18 @@ "Quickstart\n", "
\n", " \n", - "Already have a `model`? Run cross-validation to get out-of-sample `pred_probs` and then the code below to get label issue indices ranked by their inferred severity.\n", + "Already have a `model`? Run cross-validation to get out-of-sample `pred_probs` and then the code below to find any potential label errors in your dataset.\n", "\n", "\n", "
\n", " \n", "```python\n", + "from cleanlab import Datalab\n", "\n", - "from cleanlab.filter import find_label_issues\n", - "\n", - "ranked_label_issues = find_label_issues(\n", - " labels,\n", - " pred_probs,\n", - " return_indices_ranked_by=\"self_confidence\",\n", - ")\n", - " \n", + "lab = Datalab(data=your_dataset, label_name=\"column_name_of_labels\")\n", + "lab.find_issues(pred_probs=your_pred_probs, issue_types={\"label\":{}})\n", "\n", + "lab.get_issues(\"label\")\n", "```\n", " \n", "
\n", @@ -66,8 +63,8 @@ "You can use `pip` to install all packages required for this tutorial as follows:\n", "\n", "```ipython3\n", - "!pip install matplotlib torch torchvision skorch\n", - "!pip install cleanlab\n", + "!pip install matplotlib torch torchvision skorch datasets\n", + "!pip install \"cleanlab[datalab]\"\n", "# Make sure to install the version corresponding to this tutorial\n", "# E.g. if viewing master branch documentation:\n", "# !pip install git+https://github.com/cleanlab/cleanlab.git\n", @@ -86,7 +83,7 @@ "# If running on Colab, may want to use GPU (select: Runtime > Change runtime type > Hardware accelerator > GPU)\n", "# Package versions used: matplotlib==3.5.1 torch==1.11.0 skorch==0.11.0\n", "\n", - "dependencies = [\"cleanlab\", \"matplotlib\", \"torch\", \"torchvision\", \"skorch\"]\n", + "dependencies = [\"cleanlab\", \"matplotlib\", \"torch\", \"torchvision\", \"skorch\", \"datasets\"]\n", "\n", "if \"google.colab\" in str(get_ipython()): # Check if it's running in Google Colab\n", " %pip install cleanlab # for colab\n", @@ -173,12 +170,9 @@ "
\n", "Bringing Your Own Data (BYOD)?\n", "\n", - "Assign your data's features to variable `X` and its labels to variable `labels` instead.\n", + "Assign your data's features to variable `X` and its labels to variable `labels` instead, and continue with the rest of the tutorial.\n", "\n", - "Your classes (and entries of `labels`) should be represented as integer indices 0, 1, ..., num_classes - 1.\n", - "For example, if your dataset has 7 examples from 3 classes, `labels` might be: `np.array([2,0,0,1,2,0,1])`\n", - "\n", - "
\n" + "" ] }, { @@ -240,7 +234,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As some cleanlab features require scikit-learn compatibility, we adapt the above PyTorch neural net accordingly. [skorch](https://skorch.readthedocs.io) is a convenient package that helps with this. Alternatively, you can also easily wrap an arbitrary model to be scikit-learn compatible as demonstrated [here](https://github.com/cleanlab/cleanlab#use-cleanlab-with-any-model-tensorflow-pytorch-sklearn-xgboost-etc)." + "As some cleanlab features require scikit-learn compatibility, we adapt the above PyTorch neural net accordingly. [skorch](https://skorch.readthedocs.io) is a convenient package that helps with this. Alternatively, you can also easily wrap an arbitrary model to be scikit-learn compatible as demonstrated [here](https://github.com/cleanlab/cleanlab#use-cleanlab-with-any-model-for-most-ml-tasks)." ] }, { @@ -311,7 +305,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Based on the given labels and out-of-sample predicted probabilities, cleanlab can quickly help us identify label issues in our dataset. For a dataset with N examples from K classes, the labels should be a 1D array of length N and predicted probabilities should be a 2D (N x K) array. Here we request that the indices of the identified label issues be sorted by cleanlab's self-confidence score, which measures the quality of each given label via the probability assigned to it in our model's prediction." + "Based on the given labels and out-of-sample predicted probabilities, cleanlab can quickly help us identify label issues in our dataset. \n", + "\n", + "Here, we use cleanlab's `Datalab` to find potential label errors in our data. `Datalab` has several ways of loading the data. In this case, we’ll simply wrap the training features and noisy labels in a dictionary. We can instantiate our `Datalab` object with the dictionary created, and then pass in the model predicted probabilities we obtained above, and specify that we want to look for label errors by specifying that using the `issue_types` argument." ] }, { @@ -320,23 +316,58 @@ "metadata": {}, "outputs": [], "source": [ - "from cleanlab.filter import find_label_issues\n", + "from cleanlab import Datalab\n", "\n", - "ranked_label_issues = find_label_issues(\n", - " labels,\n", - " pred_probs,\n", - " return_indices_ranked_by=\"self_confidence\",\n", - ")\n", + "data = {\"X\": X, \"y\": labels}\n", + "\n", + "lab = Datalab(data, label_name=\"y\")\n", + "lab.find_issues(pred_probs=pred_probs, issue_types={\"label\":{}})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After the audit is complete, we can view the results and information regarding the labels using Datalab's `get_issues` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "issue_results = lab.get_issues(\"label\")\n", + "issue_results.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The dataframe above contains a label quality score for each example. These numeric scores lie between 0 and 1, where lower scores indicate examples more likely to be mislabeled. It contains a boolean column specifying whether or not each example is identified to have a label issue (indicating it is likely mislabeled).\n", + "\n", + "We can sort the results obtained by label score to find the indices of the 15 most likely mislabeled examples in our dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ranked_label_issues = issue_results.sort_values(\"label_score\").index\n", "\n", - "print(f\"Cleanlab found {len(ranked_label_issues)} label issues.\")\n", - "print(f\"Top 15 most likely label errors: \\n {ranked_label_issues[:15]}\")" + "print(f\"Top 15 most likely label errors: \\n {ranked_label_issues.values[:15]}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "`ranked_label_issues` is a list of indices corresponding to examples that are worth inspecting more closely. To help visualize specific examples, we define a `plot_examples` function (can skip these details)." + "`ranked_label_issues` is a list of indices ranked by the label score of each example, the top indices in the list corresponding to examples that are worth inspecting more closely. To help visualize specific examples, we define a `plot_examples` function (can skip these details)." ] }, { @@ -488,7 +519,7 @@ "source": [ "# Note: This cell is only for docs.cleanlab.ai, if running on local Jupyter or Colab, please ignore it.\n", "\n", - "highlighted_indices = [59915, 24798, 59701, 50340] # verify these examples were found by find_label_issues\n", + "highlighted_indices = [59915, 24798, 59701, 50340] # verify these examples were found by cleanlab\n", "if not all(x in ranked_label_issues for x in highlighted_indices):\n", " raise Exception(\"Some highlighted examples are missing from ranked_label_issues.\")" ] diff --git a/docs/source/tutorials/indepth_overview.ipynb b/docs/source/tutorials/indepth_overview.ipynb index 81eea482cc..11b591da9b 100644 --- a/docs/source/tutorials/indepth_overview.ipynb +++ b/docs/source/tutorials/indepth_overview.ipynb @@ -10,7 +10,7 @@ "\n", "In this tutorial, you will learn how to easily incorporate [cleanlab](https://github.com/cleanlab/cleanlab) into your ML development workflows to:\n", "\n", - "- Automatically find label issues lurking in your classification data.\n", + "- Automatically find issues such as label errors, outliers and near duplicates lurking in your classification data.\n", "- Score the label quality of every example in your dataset.\n", "- Train robust models in the presence of label issues.\n", "- Identify overlapping classes that you can merge to make the learning task less ambiguous.\n", @@ -41,7 +41,7 @@ "\n", "```\n", "!pip install sklearn matplotlib\n", - "!pip install cleanlab\n", + "!pip install cleanlab[datalab]\n", "# Make sure to install the version corresponding to this tutorial\n", "# E.g. if viewing master branch documentation:\n", "# !pip install git+https://github.com/cleanlab/cleanlab.git\n", @@ -59,7 +59,7 @@ "# Package installation (hidden on docs website).\n", "# Package versions used: matplotlib==3.5.1 \n", "\n", - "dependencies = [\"cleanlab\", \"sklearn\", \"matplotlib\"]\n", + "dependencies = [\"cleanlab\", \"sklearn\", \"matplotlib\", \"datasets\"]\n", "\n", "if \"google.colab\" in str(get_ipython()): # Check if it's running in Google Colab\n", " %pip install cleanlab # for colab\n", @@ -91,6 +91,7 @@ "source": [ "import numpy as np\n", "import cleanlab\n", + "from cleanlab import Datalab\n", "from cleanlab.classification import CleanLearning\n", "from cleanlab.benchmarking import noise_generation\n", "from sklearn.linear_model import LogisticRegression\n", @@ -334,13 +335,82 @@ "Like [many real-world datasets](https://labelerrors.com/), the given label happens to be incorrect for some of the examples (**circled in red**) in this dataset!" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Workflow 1:** Use Datalab to detect many types of issues " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Datalab offers an easy interface to detect all sorts of common real-world issue in your dataset. Internally it uses many data quality algorithms, and these methods can also be directly invoked — as demonstrated in some of the subsequent workflows here." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Datalab offers several ways of loading the data\n", + "# we’ll simply wrap the training features and noisy labels in a dictionary. \n", + "data_dict = {\"X\": data, \"y\": labels}\n", + "\n", + "# get out of sample predicted probabilities via cross-validation.\n", + "yourFavoriteModel = LogisticRegression(verbose=0, random_state=SEED)\n", + "pred_probs = cross_val_predict(\n", + " estimator=yourFavoriteModel, X=data, y=labels, cv=3, method=\"predict_proba\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All that is need to audit your data is initalize a Datalab object with your dataset and call `find_issues()`. \n", + "\n", + "Pass in the predicted probabilities and feature embeddings for your data and Datalab will do all the work!\n", + "You do not necessarily need to provide all of this information depending on which types of issues you are interested in, but the more inputs you provide, the more types of issues `Datalab` can detect in your data. Using a better model to produce these inputs will ensure cleanlab more accurately estimates issues." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lab = Datalab(data_dict, label_name=\"y\")\n", + "lab.find_issues(pred_probs=pred_probs, features=data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After the audit is complete, review the findings using the `report` method:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "lab.report()" + ] + }, { "cell_type": "markdown", "metadata": { "id": "ZmUd-5tljruT" }, "source": [ - "## **Workflow 1:** Use CleanLearning() for everything\n" + "## **Workflow 2:** Use CleanLearning for more robust Machine Learning\n" ] }, { @@ -406,7 +476,7 @@ "id": "_b8O6_J2jruU" }, "source": [ - "## **Workflow 2:** Use CleanLearning to find_label_issues in one line of code\n" + "## **Workflow 3:** Use CleanLearning to find_label_issues in one line of code\n" ] }, { @@ -473,7 +543,7 @@ "id": "XYFkRMk-jruV" }, "source": [ - "## **Workflow 3:** Use cleanlab to find dataset-level and class-level issues\n", + "## **Workflow 4:** Use cleanlab to find dataset-level and class-level issues\n", "\n", "- Did you notice that the yellow and seafoam green class above are overlapping?\n", "- How can a model ever know (or learn) what's ground truth inside the yellow distribution?\n", @@ -563,7 +633,7 @@ "id": "BxI7bgn8L_1K" }, "source": [ - "## **Workflow 4:** Clean your test set too if you're doing ML with noisy labels!" + "## **Workflow 5:** Clean your test set too if you're doing ML with noisy labels!" ] }, { @@ -618,7 +688,7 @@ "id": "GluE5XAAjruW" }, "source": [ - "## **Workflow 5:** One score to rule them all -- use cleanlab's overall dataset health score\n", + "## **Workflow 6:** One score to rule them all -- use cleanlab's overall dataset health score\n", "\n", "This score can be fairly compared across datasets or across versions of a dataset to track overall dataset quality (a.k.a. *dataset health*) over time.\n" ] @@ -679,7 +749,7 @@ "id": "8hxY5lxJjruW" }, "source": [ - "## **Workflow(s) 6:** Use count, rank, filter modules directly\n", + "## **Workflow(s) 7:** Use count, rank, filter modules directly\n", "\n", "- Using these modules directly is intended for more experienced cleanlab users. But once you understand how they work, you can create numerous powerful workflows.\n", "- For these workflows, you **always** need two things:\n", @@ -709,7 +779,7 @@ "id": "ftWk9CTrjruW" }, "source": [ - "### **Workflow 6.1 (count)**: Fully characterize label noise (noise matrix, joint, prior of true labels, ...)\n", + "### **Workflow 7.1 (count)**: Fully characterize label noise (noise matrix, joint, prior of true labels, ...)\n", "\n", "Now that we have `pred_probs` and `labels`, advanced users can compute everything in `cleanlab.count`.\n", "\n", @@ -786,7 +856,7 @@ "id": "cfeJAGyxFFQN" }, "source": [ - "### **Workflow 6.2 (filter):** Find label issues for any dataset and any model in one line of code\n", + "### **Workflow 7.2 (filter):** Find label issues for any dataset and any model in one line of code\n", "\n", "Features of ``cleanlab.filter.find_label_issues``:\n", "\n", @@ -853,7 +923,7 @@ "id": "BcekDhvFLntB" }, "source": [ - "### Workflow 6.2 supports lots of methods to ``find_label_issues()`` via the ``filter_by`` parameter.\n", + "### Workflow 7.2 supports lots of methods to ``find_label_issues()`` via the ``filter_by`` parameter.\n", "* Here, we evaluate precision/recall/f1/accuracy of detecting true label issues for each method." ] }, @@ -924,7 +994,7 @@ "id": "vNkStbegYk7y" }, "source": [ - "### **Workflow 6.3 (rank):** Automatically rank every example by a unique label quality score. Find errors using `cleanlab.count.num_label_issues` as a threshold.\n", + "### **Workflow 7.3 (rank):** Automatically rank every example by a unique label quality score. Find errors using `cleanlab.count.num_label_issues` as a threshold.\n", "\n", "cleanlab can analyze every label in a dataset and provide a numerical score gauging its overall quality. Low-quality labels indicate examples that should be more closely inspected, perhaps because their given label is incorrect, or simply because they represent an ambiguous edge-case that's worth a second look." ] @@ -982,11 +1052,11 @@ "id": "ol57ouSTNAfZ" }, "source": [ - "#### Not sure when to use Workflow 6.2 or 6.3 to find label issues?\n", + "#### Not sure when to use Workflow 7.2 or 7.3 to find label issues?\n", "\n", - "* Workflow 6.2 is the easiest to use as its just one line of code.\n", - "* Workflow 6.3 is modular and extensible. As we add more label and data quality scoring functions in ``cleanlab.rank``, Workflow 6.3 will always work.\n", - "* Workflow 6.3 is also for users who have a custom way to rank their data by label quality, and they just need to know what the cut-off is, found via ``cleanlab.count.num_label_issues``." + "* Workflow 7.2 is the easiest to use as its just one line of code.\n", + "* Workflow 7.3 is modular and extensible. As we add more label and data quality scoring functions in ``cleanlab.rank``, Workflow 7.3 will always work.\n", + "* Workflow 7.3 is also for users who have a custom way to rank their data by label quality, and they just need to know what the cut-off is, found via ``cleanlab.count.num_label_issues``." ] }, { @@ -995,7 +1065,7 @@ "id": "gRfHlDlEKyRD" }, "source": [ - "## **Workflow 7:** Ensembling label quality scores from multiple predictors" + "## **Workflow 8:** Ensembling label quality scores from multiple predictors" ] }, { @@ -1074,7 +1144,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.13" + "version": "3.9.13" } }, "nbformat": 4, diff --git a/docs/source/tutorials/index.rst b/docs/source/tutorials/index.rst index e0d63a7e06..d95e7bf8c0 100644 --- a/docs/source/tutorials/index.rst +++ b/docs/source/tutorials/index.rst @@ -4,15 +4,19 @@ Tutorials .. toctree:: :maxdepth: 1 + datalab/ indepth_overview image text tabular audio dataset_health + regression outliers multiannotator multilabel_classification token_classification + segmentation + object_detection pred_probs_cross_val faq diff --git a/docs/source/tutorials/multiannotator.ipynb b/docs/source/tutorials/multiannotator.ipynb index 2fb03e5e38..b22a2d97c9 100644 --- a/docs/source/tutorials/multiannotator.ipynb +++ b/docs/source/tutorials/multiannotator.ipynb @@ -15,7 +15,7 @@ "source": [ "This 5-minute quickstart tutorial shows how to use cleanlab for classification data that has been labeled by multiple annotators (where each example has been labeled by at least one annotator, but not every annotator has labeled every example). Compared to existing crowdsourcing tools, cleanlab helps you better analyze such data by leveraging a trained classifier model in addition to the raw annotations. With one line of code, you can automatically compute:\n", "\n", - "- A **consensus label** for each example that aggregates the individual annotations more accurately than alternative aggregation via majority-vote or other algorithms used in crowdsourcing.\n", + "- A **consensus label** for each example (i.e. *truth inference*) that aggregates the individual annotations (more accurately than algorithms from crowdsourcing like majority-vote, Dawid-Skene, or GLAD).\n", "- a **quality score for each consensus label** which measures our confidence that this label is correct (via well-calibrated estimates that account for the: number annotators which have labeled this example, overall quality of each annotator, and quality of our trained ML models).\n", "- An analogous **label quality score** for each individual label chosen by one annotator for a particular example.\n", "- An **overall quality score for each annotator** which measures our confidence in the overall correctness of labels obtained from this annotator.\n", @@ -710,14 +710,14 @@ "You can also repeatedly iterate this process of getting better consensus labels using the model's out-of-sample predicted probabilities and then retraining the model with the improved labels to get even better predicted probabilities!\n", "For details, see our [examples](https://github.com/cleanlab/examples) notebook on [Iterative use of Cleanlab to Improve Classification Models (and Consensus Labels) from Data Labeled by Multiple Annotators](https://github.com/cleanlab/examples/blob/master/multiannotator_cifar10/multiannotator_cifar10.ipynb).\n", "\n", - "If possible, the best way to improve your model is to collect additional labels for both previously annotated data and extra not-yet-labeled examples. To decide which data is most informative to label next, use `cleanlab.multiannotator.get_active_learning_scores()` rather than the methods shown here. This is demonstrated in our [examples](https://github.com/cleanlab/examples) notebook on [Active Learning with Multiple Data Annotators via CROWDLAB](https://github.com/cleanlab/examples/blob/master/active_learning_multiannotator/active_learning.ipynb).\n", + "If possible, the best way to improve your model is to collect additional labels for both previously annotated data and extra not-yet-labeled examples (i.e. *active learning*). To decide which data is most informative to label next, use `cleanlab.multiannotator.get_active_learning_scores()` rather than the methods from this tutorial. This is demonstrated in our examples notebook on [Active Learning with Multiple Data Annotators via ActiveLab](https://github.com/cleanlab/examples/blob/master/active_learning_multiannotator/active_learning.ipynb).\n", "\n", "\n", "## How does cleanlab.multiannotator work?\n", "\n", - "All estimates above are produced via the CROWDLAB algorithm, described and benchmarked in this paper:\n", + "All estimates above are produced via the CROWDLAB algorithm, described in this paper that contains extensive benchmarks which show CROWDLAB can produce better estimates than popular methods like Dawid-Skene and GLAD:\n", "\n", - "[Utilizing supervised models to infer consensus labels and their quality from data with multiple annotators](https://arxiv.org/abs/2210.06812)" + "[CROWDLAB: Supervised learning to infer consensus labels and quality scores for data with multiple annotators](https://arxiv.org/abs/2210.06812)" ] }, { diff --git a/docs/source/tutorials/multilabel_classification.ipynb b/docs/source/tutorials/multilabel_classification.ipynb index 6c40844957..d37ba8298a 100644 --- a/docs/source/tutorials/multilabel_classification.ipynb +++ b/docs/source/tutorials/multilabel_classification.ipynb @@ -19,17 +19,16 @@ "Quickstart\n", "
\n", " \n", - "cleanlab finds label issues based on two inputs: `labels` formatted as a list of lists of integer class indices that apply to each example in your dataset, and `pred_probs` from a trained multi-label classification model (which do not need to sum to 1 since the classes are not mutually exclusive). Once you have these, run the code below to find label issues in your dataset.\n", + "cleanlab finds label issues based on two inputs: `labels` formatted as a list of lists of integer class indices that apply to each example in your dataset, and `pred_probs` from a trained multi-label classification model (which do not need to sum to 1 since the classes are not mutually exclusive). Once you have these, run the code below to find label issues in your multi-label dataset:\n", "\n", "
\n", " \n", "```ipython3 \n", - "from cleanlab.filter import find_label_issues\n", + "from cleanlab.multilabel_classification.filter import find_label_issues\n", "\n", "ranked_label_issues = find_label_issues(\n", " labels=labels,\n", " pred_probs=pred_probs,\n", - " multi_label=True,\n", " return_indices_ranked_by=\"self_confidence\",\n", ")\n", "```\n", @@ -105,13 +104,9 @@ "from sklearn.model_selection import StratifiedKFold\n", "import matplotlib.pyplot as plt\n", "\n", - "from cleanlab.filter import find_label_issues\n", - "import cleanlab.internal.multilabel_utils as mlutils\n", - "from cleanlab.internal.multilabel_utils import onehot2int, int2onehot\n", - "from cleanlab.benchmarking.noise_generation import (\n", - " generate_noise_matrix_from_trace,\n", - " generate_noisy_labels,\n", - ")" + "from cleanlab.multilabel_classification.filter import find_label_issues\n", + "from cleanlab.multilabel_classification.rank import get_label_quality_scores\n", + "from cleanlab.internal.multilabel_utils import int2onehot, onehot2int" ] }, { @@ -132,6 +127,11 @@ "```ipython3\n", "# Note: This pulldown content is for docs.cleanlab.ai, if running on local Jupyter or Colab, please ignore it.\n", " \n", + "from cleanlab.benchmarking.noise_generation import (\n", + " generate_noise_matrix_from_trace,\n", + " generate_noisy_labels,\n", + ")\n", + "\n", "def make_multilabel_data(\n", " means=[[-5, 3.5], [0, 2], [-3, 6]],\n", " covs=[[[3, -1.5], [-1.5, 1]], [[5, -1.5], [-1.5, 1]], [[3, -1.5], [-1.5, 1]]],\n", @@ -255,6 +255,11 @@ }, "outputs": [], "source": [ + "from cleanlab.benchmarking.noise_generation import (\n", + " generate_noise_matrix_from_trace,\n", + " generate_noisy_labels,\n", + ")\n", + "\n", "def make_multilabel_data(\n", " means=[[-5, 3.5], [0, 2], [-3, 6]],\n", " covs=[[[3, -1.5], [-1.5, 1]], [[5, -1.5], [-1.5, 1]], [[3, -1.5], [-1.5, 1]]],\n", @@ -395,7 +400,7 @@ "source": [ "## 2. Format data, labels, and model predictions\n", "\n", - "In multi-label classification, each example in the dataset is labeled as belonging to one **or more** of *K* possible classes. To find label issues, cleanlab requires predicted class probabilities from a trained classifier. \n", + "In multi-label classification, each example in the dataset is labeled as belonging to one **or more** of *K* possible classes (or none of the classes at all). To find label issues, cleanlab requires predicted class probabilities from a trained classifier. \n", "Here we produce out-of-sample `pred_probs` by employing cross-validation to fit a multi-label **RandomForestClassifier** model via sklearn's [OneVsRestClassifier](https://scikit-learn.org/stable/modules/generated/sklearn.multiclass.OneVsRestClassifier.html) framework. `OneVsRestClassifier` offers an easy way to apply any multi-class classifier model from sklearn to multi-label classification tasks. It is done for simplicity here, but we advise against this approach as it does not properly model dependencies between classes.\n", "\n", "To instead train a state-of-the-art Pytorch neural network for multi-label classification and produce `pred_probs` on a real image dataset (that properly account for dependencies between classes), see our [example](https://github.com/cleanlab/examples) notebook [\"Train a neural network for multi-label classification on the CelebA dataset\"](https://github.com/cleanlab/examples/blob/master/multilabel_classification/pytorch_network_training.ipynb). " @@ -434,7 +439,7 @@ "\n", "`labels` should be a list of lists, whose *i*-th entry is a list of (integer) class indices that apply to the *i*-th example in the dataset. If your classes are represented as string names, you should map these to integer indices. The label for an example that belongs to none of the classes should just be an empty list `[]`.\n", "\n", - "Once you have `pred_probs` and `labels` in the appropriate formats, you can find label issues with cleanlab for any multi-label dataset!\n", + "Once you have `pred_probs` and `labels` appropriately formatted, you can find/analyze label issues in any multi-label dataset via methods from the `cleanlab.multilabel_classification` module!\n", "\n", "Here's what these look like for the first few examples in our synthetic multi-label dataset: " ] @@ -474,7 +479,6 @@ "issues = find_label_issues(\n", " labels=labels,\n", " pred_probs=pred_probs,\n", - " multi_label=True,\n", " return_indices_ranked_by=\"self_confidence\",\n", ")\n", "\n", @@ -486,8 +490,6 @@ "id": "d6af5833", "metadata": {}, "source": [ - "Note we specified the `multi_label` option above to distinguish the task from *multi-class classification* (otherwise assumed as the default task).\n", - "\n", "Let's look at the samples that cleanlab thinks are most likely to be mislabeled. You can see that cleanlab was able to identify most of `true_errors` in our small dataset (despite not having access to this variable, which you won't have in your own applications)." ] }, @@ -518,8 +520,6 @@ "metadata": {}, "outputs": [], "source": [ - "from cleanlab.multilabel_classification import get_label_quality_scores\n", - "\n", "scores = get_label_quality_scores(labels, pred_probs)\n", "\n", "print(f\"Label quality scores of the first 10 examples in dataset:\\n{scores[:10]}\")" @@ -530,6 +530,8 @@ "id": "d65af827-aeda-4b6b-9ae7-b1f0b84700d5", "metadata": {}, "source": [ + "**Note:** For multi-label data, make sure to use the versions of `find_label_issues()` and `get_label_quality_scores()` from the `cleanlab.multilabel_classification` module. There exist other versions of these methods for other types of data.\n", + "\n", "### How to format labels given as a one-hot (multi-hot) binary matrix?\n", "\n", "For multi-label classification, cleanlab expects labels to be formatted as a list of lists, where each entry is an integer corresponding to a particular class. Here are some functions you can use to easily convert labels between this format and a binary matrix format commonly used to train multi-label classification models." @@ -542,8 +544,6 @@ "metadata": {}, "outputs": [], "source": [ - "from cleanlab.internal.multilabel_utils import int2onehot, onehot2int\n", - "\n", "labels_binary_format = int2onehot(labels, K=num_class)\n", "labels_list_format = onehot2int(labels_binary_format)" ] @@ -591,7 +591,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.15" + "version": "3.9.12" } }, "nbformat": 4, diff --git a/docs/source/tutorials/object_detection.ipynb b/docs/source/tutorials/object_detection.ipynb new file mode 100644 index 0000000000..d120041c72 --- /dev/null +++ b/docs/source/tutorials/object_detection.ipynb @@ -0,0 +1,539 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d299c1e8", + "metadata": {}, + "source": [ + "# Finding Label Errors in Object Detection Datasets\n", + "\n", + "This 5-minute quickstart tutorial demonstrates how to find potential label errors in object detection datasets. In object detection data, each image is annotated with multiple bounding boxes. Each bounding box surrounds a physical object within an image scene, and is annotated with a given class label. \n", + "\n", + "Using such labeled data, we train a model to predict the locations and classes of objects in an image. An example notebook to train the object detection model whose predictions we rely on in this tutorial is available [here](https://github.com/cleanlab/examples/blob/master/object_detection/detectron2_training.ipynb). These predictions can subsequently be input to cleanlab in order to identify mislabeled images and a quality score quantifying our confidence in the overall annotations for each image. \n", + "\n", + "After correcting these label issues, **you can train an even better version of your model without changing your training code!**\n", + "\n", + "This tutorial uses a subset of the [COCO (Common Objects in Context)](https://cocodataset.org/#home) dataset which has images of everyday scenes and considers objects from the 5 most popular classes: car, chair, cup, person, traffic light.\n", + "\n", + "**Overview of what we we'll do in this tutorial**\n", + "\n", + "- Score images based on their overall label quality (i.e. our confidence each image is correctly labeled) using `cleanlab.object_detection.rank.get_label_quality_scores`\n", + "- Estimate which images have label issues using `cleanlab.object_detection.filter.find_label_issues`\n", + "- Visually review images + labels using `cleanlab.object_detection.summary.visualize`\n", + "\n", + "
\n", + "Quickstart\n", + "
\n", + " \n", + "Already have `labels` and `predictions` in the proper format? Just run the code below to find label issues in your object detection dataset.\n", + "\n", + "\n", + "
\n", + " \n", + "```python\n", + "\n", + "from cleanlab.object_detection.filter import find_label_issues\n", + "from cleanlab.object_detection.rank import get_label_quality_scores\n", + "\n", + "# To get boolean vector of label issues for all images\n", + "is_label_issue = find_label_issues(labels, predictions)\n", + "\n", + "# To get label quality scores for all images\n", + "label_quality_scores = get_label_quality_scores(labels, predictions)\n", + " \n", + " \n", + "```\n", + "\n", + "
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "8d552ab9", + "metadata": {}, + "source": [ + "## 1. Install required dependencies and download data\n", + "You can use `pip` to install all packages required for this tutorial as follows\n", + "```ipython\n", + "!pip install cleanlab\n", + "!pip insall matplotlib\n", + "# Make sure to install the version corresponding to this tutorial\n", + "# E.g. if viewing master branch documentation:\n", + "# !pip install git+https://github.com/cleanlab/cleanlab.git\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0ba0dc70", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Package installation (hidden on docs website).\n", + "dependencies = [\"cleanlab\", \"matplotlib\"]\n", + "\n", + "if \"google.colab\" in str(get_ipython()): # Check if it's running in Google Colab\n", + " %pip install cleanlab # for colab\n", + " cmd = ' '.join([dep for dep in dependencies if dep != \"cleanlab\"])\n", + " %pip install $cmd\n", + "else:\n", + " missing_dependencies = []\n", + " for dependency in dependencies:\n", + " try:\n", + " __import__(dependency)\n", + " except ImportError:\n", + " missing_dependencies.append(dependency)\n", + "\n", + " if len(missing_dependencies) > 0:\n", + " print(\"Missing required dependencies:\")\n", + " print(*missing_dependencies, sep=\", \")\n", + " print(\"\\nPlease install them before running the rest of this notebook.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c90449c8", + "metadata": {}, + "outputs": [], + "source": [ + "%%capture\n", + "\n", + "!wget -nc 'https://cleanlab-public.s3.amazonaws.com/ObjectDetectionBenchmarking/tutorial_obj/predictions.pkl'\n", + "!wget -nc 'https://cleanlab-public.s3.amazonaws.com/ObjectDetectionBenchmarking/tutorial_obj/labels.pkl'\n", + "!wget -nc 'https://cleanlab-public.s3.amazonaws.com/ObjectDetectionBenchmarking/tutorial_obj/example_images.zip' && unzip -q -o example_images.zip" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df8be4c6", + "metadata": {}, + "outputs": [], + "source": [ + "import pickle\n", + "\n", + "from cleanlab.object_detection.rank import get_label_quality_scores, issues_from_scores\n", + "from cleanlab.object_detection.filter import find_label_issues\n", + "from cleanlab.object_detection.summary import visualize" + ] + }, + { + "cell_type": "markdown", + "id": "2506badc", + "metadata": {}, + "source": [ + "## 2. Format data, labels, and model predictions\n", + "\n", + "We begin by loading `labels` and `predictions` for our dataset, which are the only inputs required to find label issues with cleanlab. Note that the predictions should be **out-of-sample**, which can be obtained for every image in a dataset via K-fold cross-validation. \n", + "\n", + "In a separate [example](https://github.com/cleanlab/examples) notebook ([link](https://github.com/cleanlab/examples/blob/master/object_detection/detectron2_training.ipynb)), we trained a Detectron2 object detection model and used it to obtain predictions on a held-out validation dataset whose `labels` we audit here.\n", + "\n", + "**Note:** If you want to find all the mislabeled images across the entire COCO dataset, you can first execute our [other example notebook](https://github.com/cleanlab/examples/blob/master/object_detection/detectron2_training-kfold.ipynb) that uses K-fold cross-validation to produce **out-of-sample** predictions for every image, then use those labels and predictions below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e9ffd6f", + "metadata": {}, + "outputs": [], + "source": [ + "IMAGE_PATH = './example_images/' # path to raw image files downloaded above\n", + "predictions = pickle.load(open(\"predictions.pkl\", \"rb\"))\n", + "labels = pickle.load(open(\"labels.pkl\", \"rb\"))" + ] + }, + { + "cell_type": "markdown", + "id": "35d49e5d", + "metadata": {}, + "source": [ + "In object detection datasets, each given label is a made up of bounding box coordinates and a class label. A model prediction is also made up of a bounding box and predicted class label, as well as the model confidence (probability estimate) in its prediction. To detect label issues, cleanlab requires given labels for each image, and the corresponding model predictions for the image (but not the image itself).\n", + "\n", + "Here’s what an example looks like in our dataset. We visualize the given and predicted labels (in red and blue) for this image using the `cleanlab.object_detection.summary.visualize` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56705562", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "image_to_visualize = 8 # change this to view other images\n", + "image_path = IMAGE_PATH + labels[image_to_visualize]['seg_map']\n", + "visualize(image_path, label=labels[image_to_visualize], prediction=predictions[image_to_visualize], overlay=False)" + ] + }, + { + "cell_type": "markdown", + "id": "ff36d97f", + "metadata": {}, + "source": [ + "The required format of these `labels` and `predictions` matches what popular object detection frameworks like [MMDetection](https://github.com/open-mmlab/mmdetection) and [Detectron2](https://github.com/facebookresearch/detectron2/) expect. Recall the 5 possible class labels in our dataset are: car, chair, cup, person, traffic light. These classes are represented as (zero-indexed) integers 0,1,...,4.\n", + "\n", + "`labels` is a list where for the i-th image in our dataset, `labels[i]` is a dictionary containing: key `labels` -- a list of class labels for each bounding box in this image and key `bboxes` -- a numpy array of the bounding boxes' coordinates. Each bounding box in `labels[i]['bboxes']` is in the format ``[x1,y1,x2,y2]`` where `(x1,y1)` corresponds to the bottom-left corner of the box and `(x2,y2)` the top-right. \n", + "\n", + "Let's see what `labels[i]` looks like for our previous example image:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b08144d7", + "metadata": {}, + "outputs": [], + "source": [ + "labels[image_to_visualize]" + ] + }, + { + "cell_type": "markdown", + "id": "8f62da67", + "metadata": {}, + "source": [ + "`predictions` is a list where the predictions output by our model for the i-th image: `predictions[i]` is a list/array of shape `(K,)`. Here `K` is the number of classes in the dataset (same for every image) and `predictions[i][k]` is of shape `(M,5)`, where `M` is the number of bounding boxes predicted to contain objects of class `k` (in image i, differs between images). The five columns of `predictions[i][k]` correspond to ``[x1,y1,x2,y2,pred_prob]`` for each bounding box predicted by the model. `pred_prob` is the model confidence in its predicted label for this box. Since our dataset has `K = 5` classes, we have: `predictions[i].shape = (5,)`.\n", + "\n", + "Let's see what `predictions[i]` looks like for our previous example image:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d70bec6", + "metadata": {}, + "outputs": [], + "source": [ + "predictions[image_to_visualize]" + ] + }, + { + "cell_type": "markdown", + "id": "cf95ea28", + "metadata": {}, + "source": [ + "\n", + "Once you have `labels` and `predictions` in the appropriate formats, you can **find label issues with cleanlab for any object detection dataset**!" + ] + }, + { + "cell_type": "markdown", + "id": "3daff923", + "metadata": {}, + "source": [ + "## 3. Use cleanlab to find label issues\n", + "Given `labels` and `predictions` from our trained model, cleanlab can automatically find mislabeled images in the dataset. In object detection, we consider an image mislabeled if **any** of its bounding boxes or their class labels are incorrect (including if the image contains any overlooked objects which should've been annotated with a box)\n", + "\n", + "Images may be mislabeled because annotators:\n", + "\n", + "- overlooked an object (forgot to annotate a bounding box around a depicted object)\n", + "- chose the wrong class label for an annotated box in the correct location\n", + "- imperfectly drew the bounding box such that its location is incorrect\n", + "\n", + "\n", + "Cleanlab is expected to flag images that exhibit **any** of these annotation errors as having label issues. More severe annotation errors are expected to produce lower cleanlab label quality scores closer to 0. Let's first estimate which images have label issues:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4caa635d", + "metadata": {}, + "outputs": [], + "source": [ + "label_issue_idx = find_label_issues(labels, predictions, return_indices_ranked_by_score=True)\n", + "\n", + "num_examples_to_show = 5 # view this many images flagged with the most severe label issues\n", + "label_issue_idx[:num_examples_to_show]" + ] + }, + { + "cell_type": "markdown", + "id": "66d5fae1", + "metadata": {}, + "source": [ + "The above code identifies *which* images have label issues, returning a list of their indices. This is because we specified the `return_indices_ranked_by_score` argument which sorts these indices by the estimated label quality of each image. Below we describe how to directly estimate the label quality scores of each image.\n", + "\n", + "**Note:** You can omit the `return_indices_ranked_by_score` argument for `find_label_issues()` to instead return a Boolean mask for the entire dataset (True entries in this mask correspond to images with label issues)" + ] + }, + { + "cell_type": "markdown", + "id": "5b501dc9", + "metadata": {}, + "source": [ + "### Get label quality scores\n", + "Cleanlab can also compute scores for each image to estimate our confidence that it has been correctly labeled. These label quality scores range between 0 and 1, with *smaller* values indicating examples whose annotation is *more* likely to be wrong in some way.\n", + "\n", + "Each image in the dataset receives a label quality score. These scores are useful for prioritizing which images to review; if you have too little time, first review the images with the lowest label quality scores." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a9b4c590", + "metadata": {}, + "outputs": [], + "source": [ + "scores = get_label_quality_scores(labels, predictions)\n", + "scores[:num_examples_to_show]" + ] + }, + { + "cell_type": "markdown", + "id": "349521e0", + "metadata": {}, + "source": [ + "We can also use the label quality scores to flag *which* images have label issues based on a threshold. Here we convert these per-image scores into an array of indices corresponding to images flagged with label issues, sorted by label quality score, in the same format returned by `find_label_issues()`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ffd9ebcc", + "metadata": {}, + "outputs": [], + "source": [ + "issue_idx = issues_from_scores(scores, threshold=0.5) # lower threshold will return fewer (but more confident) label issues\n", + "issue_idx[:num_examples_to_show], scores[issue_idx][:num_examples_to_show]" + ] + }, + { + "cell_type": "markdown", + "id": "5a3b8aa0", + "metadata": {}, + "source": [ + "## 4. Use ObjectLab to visualize label issues\n", + "Finally, we can visualize images with potential label errors via cleanlab's `visualize()` function. To enhance the visualization, you can supply a `class_names` dictionary to include as a legend and turn off `overlay` to see the given and predicted labels side by side." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4dd46d67", + "metadata": {}, + "outputs": [], + "source": [ + "issue_to_visualize = issue_idx[7] # change this to view other images\n", + "class_names = {\"0\": \"car\", \"1\": \"chair\", \"2\": \"cup\", \"3\":\"person\", \"4\": \"traffic light\"}\n", + "\n", + "label = labels[issue_to_visualize]\n", + "prediction = predictions[issue_to_visualize]\n", + "score = scores[issue_to_visualize]\n", + "image_path = IMAGE_PATH + label['seg_map']\n", + "\n", + "print(image_path, '| idx', issue_to_visualize , '| label quality score:', score, '| is issue: True')\n", + "visualize(image_path, label=label, prediction=prediction, class_names=class_names, overlay=False)" + ] + }, + { + "cell_type": "markdown", + "id": "de0d7205", + "metadata": {}, + "source": [ + "The visualization depicts the given label (original image annotation which cleanlab identified as problematic) in red on the left and the model-predicted label in blue on the right. Each bounding box contains a class-index number in the top corner indicating which object class that bounding box was annotated/predicted to contain.\n", + "\n", + "This image has a **low** label quality score and is marked as an error. On closer inspection we notice the annotator missed a car in the back right corner that the model identified. Additionally, the bounding box for the foreground car the woman is in is cut short a bit on the right. \n", + "\n", + "Notice examples where the predictions and labels are more similar have higher quality scores than those that are missmatched, and are less likeley to be marked as issues and the number of boxes is agnostic to the score.\n", + "\n", + "Since our model is not perfect, it incorrectly identified the passenger dog as a person however it was still able to identify issues in the scene. Better trained models will lead to better label error detection but you don't need a near perfect model to identify label issues.\n", + "\n", + "\n", + "### Different kinds of label issues identified by ObjectLab\n", + "Now lets view the first few images in our vaidation dataset that are clearly marked as issues and see what various inconsistencies between the `given` and `predicted` label we can spot. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ceec2394", + "metadata": {}, + "outputs": [], + "source": [ + "issue_to_visualize = issue_idx[3]\n", + "label = labels[issue_to_visualize]\n", + "prediction = predictions[issue_to_visualize]\n", + "score = scores[issue_to_visualize]\n", + "\n", + "image_path = IMAGE_PATH + label['seg_map']\n", + "print(image_path, '| idx', issue_to_visualize , '| label quality score:', score, '| is issue: True')\n", + "visualize(image_path, label=label, prediction=prediction, class_names=class_names, overlay=False)" + ] + }, + { + "cell_type": "markdown", + "id": "9b5c87fa", + "metadata": {}, + "source": [ + "The car in the foreground and the chair in the front yard are missing annotations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94f82b0d", + "metadata": {}, + "outputs": [], + "source": [ + "issue_to_visualize = issue_idx[1]\n", + "label = labels[issue_to_visualize]\n", + "prediction = predictions[issue_to_visualize]\n", + "score = scores[issue_to_visualize]\n", + "\n", + "image_path = IMAGE_PATH + label['seg_map']\n", + "print(image_path, '| idx', issue_to_visualize , '| label quality score:', score, '| is issue: True')\n", + "visualize(image_path, label=label, prediction=prediction, class_names=class_names, overlay=False)" + ] + }, + { + "cell_type": "markdown", + "id": "05610be0", + "metadata": {}, + "source": [ + "The monitor in this image is missing annotations for a cup and a person -- who is working on a chair in the background. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ea18c5d", + "metadata": {}, + "outputs": [], + "source": [ + "issue_to_visualize = issue_idx[5]\n", + "label = labels[issue_to_visualize]\n", + "prediction = predictions[issue_to_visualize]\n", + "score = scores[issue_to_visualize]\n", + "\n", + "image_path = IMAGE_PATH + label['seg_map']\n", + "print(image_path, '| idx', issue_to_visualize , '| label quality score:', score, '| is issue: True')\n", + "visualize(image_path, label=label, prediction=prediction, class_names=class_names, overlay=False)" + ] + }, + { + "cell_type": "markdown", + "id": "31ba0195", + "metadata": {}, + "source": [ + "The birthday party in this image should have had individual bounding boxes around each kid (the COCO guidelines state only groups with 10+ objects of the same type can be a \\\"crowd\\\" bounded by a single box). \n", + "\n", + "All of these examples received low label quality scores reflecting their low annotation quality in the original dataset." + ] + }, + { + "cell_type": "markdown", + "id": "03d5a521", + "metadata": {}, + "source": [ + "### Other uses of visualize\n", + "The `visualize()` function can also depict non-issue images, labels or predictions alone, or just the image itself. Let's explore this with a few images in our dataset.\n", + "\n", + "We can save a visualization to file via the `save_path` argument. Note the label quality score is high for this example and it is marked as a non-issue. The given and predicted labels closely resemble each other contributing to the high score." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e770d23", + "metadata": {}, + "outputs": [], + "source": [ + "image_to_visualize = 0\n", + "image_path = IMAGE_PATH + labels[image_to_visualize]['seg_map']\n", + "print(image_path, '| idx', image_to_visualize , '| label quality score:', scores[image_to_visualize], '| is issue:', image_to_visualize in issue_idx)\n", + "visualize(image_path, label=labels[image_to_visualize], prediction=predictions[image_to_visualize], class_names=class_names, save_path='./example_image.png')" + ] + }, + { + "cell_type": "markdown", + "id": "6c9464e8", + "metadata": {}, + "source": [ + "For the next example, notice how we are only passing in the given labels to visualize. We can limit visualization to either labels, predictions, or neither." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57e84a27", + "metadata": {}, + "outputs": [], + "source": [ + "image_to_visualize = 1\n", + "image_path = IMAGE_PATH + labels[image_to_visualize]['seg_map']\n", + "print(image_path, '| idx', image_to_visualize , '| label quality score:', scores[image_to_visualize], '| is issue:', image_to_visualize in issue_idx)\n", + "visualize(image_path, label=labels[image_to_visualize], class_names=class_names)" + ] + }, + { + "cell_type": "markdown", + "id": "d8744ab9", + "metadata": {}, + "source": [ + "For completeness, let's just look at an image alone." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0302818a", + "metadata": {}, + "outputs": [], + "source": [ + "image_to_visualize = 2\n", + "image_path = IMAGE_PATH + labels[image_to_visualize]['seg_map']\n", + "print(image_path, '| idx', image_to_visualize , '| label quality score:', score, '| is issue:', image_to_visualize in issue_idx)\n", + "visualize(image_path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8ce74938", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Note: This cell is only for docs.cleanlab.ai, if running on local Jupyter or Colab, please ignore it.\n", + "import numpy as np\n", + "\n", + "assert 30 in issue_idx and issue_idx[3] == 30\n", + "assert 48 in issue_idx and issue_idx[1] == 48\n", + "assert 1 in issue_idx and issue_idx[5] == 1\n", + "\n", + "assert 2 not in issue_idx and 0 not in issue_idx\n", + "assert issue_idx[7] == 32" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/tutorials/outliers.ipynb b/docs/source/tutorials/outliers.ipynb index f4888055f1..8f3fe5df71 100644 --- a/docs/source/tutorials/outliers.ipynb +++ b/docs/source/tutorials/outliers.ipynb @@ -420,7 +420,7 @@ "source": [ "### Scoring outliers in a given dataset (training data)\n", "\n", - "Fitting cleanlab's ``OutOfDistribution`` class on ``feature_embeddings`` will find any naturally occuring outliers in a given dataset. These examples are atypical images that look strange or different from the majority of examples in the dataset. In our case, these correspond to odd-looking images of animals that do not resemble typical animals depicted in **cifar10**. This method produces a score in [0,1] for each example, where lower values correspond to more atypical examples (more likely out-of-distribution)." + "Fitting cleanlab's ``OutOfDistribution`` class on ``feature_embeddings`` will find any naturally occurring outliers in a given dataset. These examples are atypical images that look strange or different from the majority of examples in the dataset. In our case, these correspond to odd-looking images of animals that do not resemble typical animals depicted in **cifar10**. This method produces a score in [0,1] for each example, where lower values correspond to more atypical examples (more likely out-of-distribution)." ] }, { diff --git a/docs/source/tutorials/regression.ipynb b/docs/source/tutorials/regression.ipynb new file mode 100644 index 0000000000..80fd39e27d --- /dev/null +++ b/docs/source/tutorials/regression.ipynb @@ -0,0 +1,644 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ea0a577e", + "metadata": {}, + "source": [ + "# Find Noisy Labels in Regression Datasets" + ] + }, + { + "cell_type": "markdown", + "id": "e15b9f2f", + "metadata": {}, + "source": [ + "This 5-minute quickstart tutorial uses cleanlab to find potentially incorrect numeric values in a dataset column by means of a regression model. Unlike classification models, regression predicts continuous quantities such as price, income, age,... To find corrupted values in a column, we treat it as the target value, i.e. label, to be predicted by the regression model and then use cleanlab to decide when the model predictions are trustworthy while deviating from the observed label value. In this tutorial, we consider a Grades dataset, which records three exam grades and some optional notes for over 900 students, each being assigned a final score. Combined with any regression model of your choosing, cleanlab automatically identifies examples in this dataset that have incorrect final scores.\n", + "\n", + "**Overview of what we’ll do in this tutorial:**\n", + "\n", + "- Fit a simple Gradient Boosting model (any other model could be used) on the exam-score and notes (covariates) in order to compute out-of-sample predictions of the final grade (the response variable in our regression).\n", + "- Use cleanlab's `CleanLearning.find_label_issues()` method to identify potentially incorrect final grade values based on outputs from this regression model.\n", + "- Train a more robust version of the same model after dropping the identified label errors using CleanLearning." + ] + }, + { + "cell_type": "markdown", + "id": "612a355a", + "metadata": {}, + "source": [ + "
\n", + "Quickstart\n", + "
\n", + " \n", + "Already have an sklearn-compatible regression `model`, features/covariates `X`, and a label/target variable `y`? Run the code below to train your `model` and identify potentially incorrect `y` values in your dataset.\n", + "\n", + "\n", + "
\n", + " \n", + "```python\n", + "\n", + "from cleanlab.regression.learn import CleanLearning\n", + "\n", + "cl = CleanLearning(model)\n", + "cl.fit(X, y)\n", + "label_issues = cl.get_label_issues()\n", + "preds = cl.predict(X_test) # predictions from a version of your model trained on auto-cleaned data\n", + "```\n", + " \n", + "
\n", + " \n", + "Is your model/data not compatible with `CleanLearning`? You can instead run cross-validation on your model to get out-of-sample `predictions`. Then run the code below to get label quality scores for each example that infer the degree of corruption in each `y` value.\n", + "\n", + "
\n", + " \n", + "```python\n", + "\n", + "from cleanlab.regression.rank import get_label_quality_scores\n", + " \n", + "label_quality_scores = get_label_quality_scores(y, predictions)\n", + " \n", + "```\n", + " \n", + "
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "f9a290d6", + "metadata": {}, + "source": [ + "## 1. Install required dependencies" + ] + }, + { + "cell_type": "markdown", + "id": "8430ca39", + "metadata": {}, + "source": [ + "You can use `pip` to install all packages required for this tutorial as follows:\n", + "\n", + "```ipython3\n", + "!pip install scikit-learn\n", + "!pip install cleanlab\n", + "# Make sure to install the version corresponding to this tutorial\n", + "# E.g. if viewing master branch documentation:\n", + "# !pip install git+https://github.com/cleanlab/cleanlab.git\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e1af7d8", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Package installation (hidden on docs website).\n", + "# Package versions we used: scikit-learn\n", + "\n", + "dependencies = [\"cleanlab\", \"sklearn\"]\n", + "\n", + "if \"google.colab\" in str(get_ipython()): # Check if it's running in Google Colab\n", + " %pip install cleanlab # for colab\n", + " cmd = \" \".join([dep for dep in dependencies if dep != \"cleanlab\"])\n", + " %pip install $cmd\n", + "else:\n", + " missing_dependencies = []\n", + " for dependency in dependencies:\n", + " try:\n", + " __import__(dependency)\n", + " except ImportError:\n", + " missing_dependencies.append(dependency)\n", + "\n", + " if len(missing_dependencies) > 0:\n", + " print(\"Missing required dependencies:\")\n", + " print(*missing_dependencies, sep=\", \")\n", + " print(\"\\nPlease install them before running the rest of this notebook.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4fb10b8f", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from sklearn.ensemble import HistGradientBoostingRegressor\n", + "from sklearn.model_selection import cross_val_predict\n", + "from sklearn.metrics import r2_score\n", + "\n", + "from cleanlab.regression.learn import CleanLearning" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "284dc264", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# This cell is hidden from docs.cleanlab.ai \n", + "\n", + "import random \n", + "import numpy as np \n", + "\n", + "SEED = 111 # for reproducibility \n", + "\n", + "np.random.seed(SEED)\n", + "random.seed(SEED)" + ] + }, + { + "cell_type": "markdown", + "id": "2035042e", + "metadata": {}, + "source": [ + "## 2. Load and process the data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f7450db", + "metadata": {}, + "outputs": [], + "source": [ + "train_data = pd.read_csv(\"https://s.cleanlab.ai/student_grades_r/train.csv\")\n", + "test_data = pd.read_csv(\"https://s.cleanlab.ai/student_grades_r/test.csv\")\n", + "train_data.head()" + ] + }, + { + "cell_type": "markdown", + "id": "aa0165ef", + "metadata": {}, + "source": [ + "In the DataFrame above, `final_score` represents the noisy scores and `true_final_score` represents the ground truth. Note that ground truth is usually not available in real-world datasets, and is added in this dataset for comparison and demonstration purposes." + ] + }, + { + "cell_type": "markdown", + "id": "82285102", + "metadata": {}, + "source": [ + "We show a 3D scatter plot of the exam grades, with the color hue corresponding to the final score for each student. Incorrect datapoints are marked with an **X**." + ] + }, + { + "cell_type": "markdown", + "id": "c8173840", + "metadata": {}, + "source": [ + "
See the code to visualize the data. **(click to expand)**\n", + " \n", + "```ipython3\n", + "# Note: This pulldown content is for docs.cleanlab.ai, if running on local Jupyter or Colab, please ignore it.\n", + " \n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from mpl_toolkits.mplot3d import Axes3D\n", + "\n", + "def plot_data(train_data, errors_idx):\n", + " fig = plt.figure()\n", + " ax = fig.add_subplot(111, projection='3d')\n", + "\n", + " x, y, z = train_data[\"exam_1\"], train_data[\"exam_2\"], train_data[\"exam_3\"]\n", + " labels = train_data[\"final_score\"]\n", + "\n", + " img = ax.scatter(x, y, z, c=labels, cmap=\"jet\")\n", + " fig.colorbar(img)\n", + "\n", + " ax.plot(\n", + " x.iloc[errors_idx],\n", + " y.iloc[errors_idx],\n", + " z.iloc[errors_idx],\n", + " \"x\",\n", + " markeredgecolor=\"black\",\n", + " markersize=10,\n", + " markeredgewidth=2.5,\n", + " alpha=0.8,\n", + " label=\"Label Errors\"\n", + " )\n", + " ax.legend()\n", + "```\n", + " \n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55513fed", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from mpl_toolkits.mplot3d import Axes3D\n", + "\n", + "def plot_data(train_data, errors_idx):\n", + " fig = plt.figure()\n", + " ax = fig.add_subplot(111, projection='3d')\n", + "\n", + " x, y, z = train_data[\"exam_1\"], train_data[\"exam_2\"], train_data[\"exam_3\"]\n", + " labels = train_data[\"final_score\"]\n", + "\n", + " img = ax.scatter(x, y, z, c=labels, cmap=\"jet\")\n", + " fig.colorbar(img)\n", + "\n", + " ax.plot(\n", + " x.iloc[errors_idx],\n", + " y.iloc[errors_idx],\n", + " z.iloc[errors_idx],\n", + " \"x\",\n", + " markeredgecolor=\"black\",\n", + " markersize=10,\n", + " markeredgewidth=2.5,\n", + " alpha=0.8,\n", + " label=\"Label Errors\"\n", + " )\n", + " ax.legend()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df5a0f59", + "metadata": {}, + "outputs": [], + "source": [ + "errors_mask = train_data[\"final_score\"] != train_data[\"true_final_score\"]\n", + "errors_idx = np.where(errors_mask == 1)\n", + "\n", + "plot_data(train_data, errors_idx)" + ] + }, + { + "cell_type": "markdown", + "id": "add939ae", + "metadata": {}, + "source": [ + "Next we preprocess the data by applying one-hot encoding to features with categorical data (this is optional if your regression model can work directly with categorical features)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7af78a8a", + "metadata": {}, + "outputs": [], + "source": [ + "feature_columns = [\"exam_1\", \"exam_2\", \"exam_3\", \"notes\"]\n", + "predicted_column = \"final_score\"\n", + "\n", + "X_train_raw, y_train = train_data[feature_columns], train_data[predicted_column]\n", + "X_test_raw, y_test = test_data[feature_columns], test_data[predicted_column]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9556c624", + "metadata": {}, + "outputs": [], + "source": [ + "categorical_features = [\"notes\"]\n", + "X_train = pd.get_dummies(X_train_raw, columns=categorical_features)\n", + "X_test = pd.get_dummies(X_test_raw, columns=categorical_features)" + ] + }, + { + "cell_type": "markdown", + "id": "1ce924cf", + "metadata": {}, + "source": [ + "
\n", + "Bringing Your Own Data (BYOD)?\n", + "\n", + "Assign your data's features to variable `X` and the target values to variable `y` instead, then continue with the rest of the tutorial.\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "4b14309d", + "metadata": {}, + "source": [ + "## 3. Define a regression model and use cleanlab to find potential label errors" + ] + }, + { + "cell_type": "markdown", + "id": "81ee2349", + "metadata": {}, + "source": [ + "`CleanLearning` provides a wrapper class for any scikit-learn compatible regression model, which can be used to find potential label issues (i.e. noisy `y`-values) and train a more robust version of the same model if the original data contains noisy labels.\n", + "\n", + "Here we define a `CleanLearning` object with a histogram-based gradient boosting model (sklearn version of XGBoost) and use `find_label_issues` to identify potential errors in our dataset's numeric label column. Any other sklearn-compatible regression model could be used, such as LinearRegression or RandomForestRegressor (or you can easily wrap arbitrary models to be compatible with the sklearn API)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3c2f1ccc", + "metadata": {}, + "outputs": [], + "source": [ + "model = HistGradientBoostingRegressor()\n", + "cl = CleanLearning(model)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e1b7860", + "metadata": {}, + "outputs": [], + "source": [ + "label_issues = cl.find_label_issues(X_train, y_train)" + ] + }, + { + "cell_type": "markdown", + "id": "43bd6c7f", + "metadata": {}, + "source": [ + "`CleanLearning` fits multiple copies of our regression model via cross-validation and bootstrapping in order to compute predictions and uncertainty estimates for the dataset, which are used to identify label issues (i.e. likely corrupted `y`-values).\n", + "\n", + "This method returns a Dataframe containing a label quality score (between 0 and 1) for each example in your dataset. Lower scores indicate examples more likely to be mislabeled with an erroneous `y` value. The Dataframe also contains a boolean column specifying whether or not each example is identified to have a label issue (indicating its `y`-value appears corrupted). " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f407bd69", + "metadata": {}, + "outputs": [], + "source": [ + "label_issues.head()" + ] + }, + { + "cell_type": "markdown", + "id": "4ab5acf3", + "metadata": {}, + "source": [ + "We can get the subset of examples flagged with label issues, and also sort by label quality score to find the indices of the 10 most likely mislabeled examples in our dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f7385336", + "metadata": {}, + "outputs": [], + "source": [ + "identified_issues = label_issues[label_issues[\"is_label_issue\"] == True]\n", + "lowest_quality_labels = label_issues[\"label_quality\"].argsort()[:10].to_numpy()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59fc3091", + "metadata": {}, + "outputs": [], + "source": [ + "print(\n", + " f\"cleanlab found {len(identified_issues)} potential label errors in the dataset.\\n\"\n", + " f\"Here are indices of the top 10 most likely errors: \\n {lowest_quality_labels}\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "aa2c1fec", + "metadata": {}, + "source": [ + "Let’s review some of the most likely label errors. To help us inspect these datapoints, we define a method to print any example from the dataset, together with its given (original) label and the suggested alternative label from cleanlab." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "00949977", + "metadata": {}, + "outputs": [], + "source": [ + "def view_datapoint(index):\n", + " given_labels = label_issues[\"given_label\"]\n", + " predicted_labels = label_issues[\"predicted_label\"].round(1)\n", + " return pd.concat(\n", + " [X_train_raw, given_labels, predicted_labels], axis=1\n", + " ).iloc[index]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b6c1ae3a", + "metadata": {}, + "outputs": [], + "source": [ + "view_datapoint(lowest_quality_labels[:5])" + ] + }, + { + "cell_type": "markdown", + "id": "f2be7a93", + "metadata": {}, + "source": [ + "These are very clear errors that cleanlab has identified in this data! Note that the `given_label` does not correctly reflect the final grade that these student should be getting. \n", + "\n", + "cleanlab has shortlisted the most likely label errors to speed up your data cleaning process. With this list, you can decide whether to fix these label issues or remove erroneous examples from the dataset." + ] + }, + { + "cell_type": "markdown", + "id": "e2761486", + "metadata": {}, + "source": [ + "## 4. Train a more robust model from noisy labels" + ] + }, + { + "cell_type": "markdown", + "id": "043bfb52", + "metadata": {}, + "source": [ + "Fixing the label issues manually may be time-consuming, but cleanlab can filter these noisy examples and train a model on the remaining clean data for you automatically.\n", + "\n", + "To establish a baseline, let’s first train and evaluate our original Gradient Boosting model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31c704e7", + "metadata": {}, + "outputs": [], + "source": [ + "baseline_model = HistGradientBoostingRegressor() \n", + "baseline_model.fit(X_train, y_train)\n", + "\n", + "preds_og = baseline_model.predict(X_test)\n", + "r2_og = r2_score(y_test, preds_og)\n", + "print(f\"r-squared score of original model: {r2_og:.3f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "0d01f715", + "metadata": {}, + "source": [ + "Now that we have a baseline, let’s check if using `CleanLearning` improves our test accuracy.\n", + "\n", + "`CleanLearning` provides a wrapper that can be applied to any scikit-learn compatible model. The resulting model object can be used in the same manner, but it will now train more robustly if the data has noisy labels.\n", + "\n", + "We can use the same `CleanLearning` object defined above, and pass the label issues we already computed into `.fit()` via the `label_issues` argument. This accelerates things; if we did not provide the label issues, then they would be re-estimated via cross-validation. After the issues are estimated, `CleanLearning` simply removes the examples with label issues and retrains your model on the remaining clean data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0bcc43db", + "metadata": {}, + "outputs": [], + "source": [ + "found_label_issues = cl.get_label_issues()\n", + "cl.fit(X_train, y_train, label_issues=found_label_issues)\n", + "\n", + "preds_cl = cl.predict(X_test)\n", + "r2_cl = r2_score(y_test, preds_cl)\n", + "print(f\"r-squared score of cleanlab's model: {r2_cl:.3f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "3aea51da", + "metadata": {}, + "source": [ + "We can see that the coefficient of determination (r-squared score) of the test set improved as a result of the data cleaning. Note that this will not always be the case, especially when we are evaluating on test data that are themselves noisy. The best practice is to run cleanlab to identify potential label issues and then manually review them, before blindly trusting any evaluation metrics. In particular, the most effort should be made to ensure high-quality test data, which is supposed to reflect the expected performance of our model during deployment." + ] + }, + { + "cell_type": "markdown", + "id": "167fca90", + "metadata": {}, + "source": [ + "## 5. Other workflows to find label issues in regression datasets" + ] + }, + { + "cell_type": "markdown", + "id": "5b4f8e14", + "metadata": {}, + "source": [ + "The `CleanLearning` workflow above requires a sklearn-compatible model, if your model or data format is not compatible with the requirements for using `CleanLearning`, you can instead run cross-validation on your model to get out-of-sample predictions, then use the `cleanlab.regression.rank.get_label_quality_scores` method to obtain label quality scores for each example.\n", + "\n", + "This method only requires two inputs:\n", + "\n", + "- `labels`: numpy array of given labels in the dataset. \n", + "- `predictions`: numpy array of predictions generated through your favorite model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7021bd68", + "metadata": {}, + "outputs": [], + "source": [ + "# get predictions using cross-validation\n", + "model = HistGradientBoostingRegressor()\n", + "predictions = cross_val_predict(estimator=model, X=X_train, y=y_train)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d49c990b", + "metadata": {}, + "outputs": [], + "source": [ + "from cleanlab.regression.rank import get_label_quality_scores\n", + "\n", + "label_quality_scores = get_label_quality_scores(labels=y_train, predictions=predictions)\n", + "label_quality_scores[:5]" + ] + }, + { + "cell_type": "markdown", + "id": "3a0db9b2", + "metadata": {}, + "source": [ + "As before, these label quality scores are continuous values in the range [0,1] where 1 represents a clean label (given label appears correct) and 0 a represents dirty label (given label appears corrupted). You can sort examples by their label quality scores to inspect the most-likely corrupted datapoints.\n", + "\n", + "If possible, we recommend you use `CleanLearning` over `rank.get_label_quality_scores` for the most accurate label error detection. To understand how these approaches work, refer to our paper: **[Detecting Errors in Numerical Data via any Regression Model](https://arxiv.org/abs/2305.16583)**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95531cda", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Note: This cell is only for docs.cleanlab.ai, if running on local Jupyter or Colab, please ignore it.\n", + "from sklearn.metrics import roc_auc_score\n", + "\n", + "if r2_cl <= r2_og:\n", + " raise ValueError(\"CleanLearning did not improve r2 score\")\n", + "\n", + "label_quality_score_cl = label_issues[\"label_quality\"]\n", + "label_quality_scores_residual = get_label_quality_scores(labels=y_train, predictions=predictions, method=\"residual\")\n", + "\n", + "auc_outre = roc_auc_score(errors_mask, 1 - label_quality_scores)\n", + "auc_cl = roc_auc_score(errors_mask, 1 - label_quality_score_cl)\n", + "auc_residual = roc_auc_score(errors_mask, 1 - label_quality_scores_residual)\n", + "\n", + "if auc_outre <= 0.5 or auc_cl <= 0.5:\n", + " raise ValueError(\"Label quality scores did not perform well enough\")\n", + "\n", + "if auc_outre <= auc_residual:\n", + " raise ValueError(\"Outre label quality scores did not outperform alternative scores\")\n", + " \n", + "if auc_cl <= auc_residual:\n", + " raise ValueError(\"CL label quality scores did not outperform alternative scores\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/tutorials/segmentation.ipynb b/docs/source/tutorials/segmentation.ipynb new file mode 100644 index 0000000000..935e487d08 --- /dev/null +++ b/docs/source/tutorials/segmentation.ipynb @@ -0,0 +1,484 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d0d2e007", + "metadata": {}, + "source": [ + "# Find Label Errors in Semantic Segmentation Datasets\n", + "\n", + "This 5-minute quickstart tutorial shows how you can use cleanlab to find potentially mislabeled images in semantic segmentation datasets. In semantic segmentation tasks, our data consists of images each annotated with a corresponding mask that labels each pixel in the image as one of K classes. Models are trained on this labeled mask to predict the class of each pixel in an image. However in real-world data, this annotated mask often contains errors. \n", + "Here we apply cleanlab to find label errors in a variant of the [SYNTHIA](https://synthia-dataset.net) segmentation dataset, which consists of synthetic images generated via graphics engine." + ] + }, + { + "cell_type": "markdown", + "id": "07936a54", + "metadata": {}, + "source": [ + "
\n", + "Quickstart\n", + "
\n", + " \n", + "cleanlab uses two inputs to handle semantic segmentation data classification data:\n", + "- `labels`: Array of dimension (N,H,W) where N is the number of images and H and W are dimension of the image. We assume an integer encoded image. For one-hot encoding one can `np.argmax(labels_one_hot,axis=1)` assuming that `labels_one_hot` is of dimension (N,K,H,W) where K is the number of classes.\n", + "- `pred_probs`: Array of dimension (N,K,H,W), similar to `labels`.\n", + "\n", + "With these inputs, you can find and review label issues via this code: \n", + "\n", + "
\n", + " \n", + "```python\n", + "\n", + "from cleanlab.semantic_segmentation.filter import find_label_issues \n", + "from cleanlab.semantic_segmentation.summary import display_issues\n", + " \n", + "issues = find_label_issues(labels, pred_probs)\n", + "display_issues(issues, pred_probs=pred_probs, labels=labels,\n", + " top=10)\n", + "\n", + "```\n", + " \n", + "
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "1da020bc", + "metadata": {}, + "source": [ + "## 1. Install required dependencies and download data\n", + "\n", + "You can use `pip` to install all packages required for this tutorial as follows: \n", + "\n", + " !pip install cleanlab " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae8a08e0", + "metadata": {}, + "outputs": [], + "source": [ + "%%capture\n", + "!wget -nc 'https://cleanlab-public.s3.amazonaws.com/ImageSegmentation/given_masks.npy' " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58fd4c55", + "metadata": {}, + "outputs": [], + "source": [ + "%%capture\n", + "!wget -nc 'https://cleanlab-public.s3.amazonaws.com/ImageSegmentation/predicted_masks.npy' " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "439b0305", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Package installation (hidden on docs website).\n", + "\n", + "dependencies = [\"cleanlab\"]\n", + "\n", + "if \"google.colab\" in str(get_ipython()): # Check if it's running in Google Colab\n", + " %pip install cleanlab # for colab\n", + " cmd = ' '.join([dep for dep in dependencies if dep != \"cleanlab\"])\n", + " %pip install $cmd\n", + "else:\n", + " missing_dependencies = []\n", + " for dependency in dependencies:\n", + " try:\n", + " __import__(dependency)\n", + " except ImportError:\n", + " missing_dependencies.append(dependency)\n", + "\n", + " if len(missing_dependencies) > 0:\n", + " print(\"Missing required dependencies:\")\n", + " print(*missing_dependencies, sep=\", \")\n", + " print(\"\\nPlease install them before running the rest of this notebook.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1349304", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from cleanlab.segmentation.filter import find_label_issues \n", + "from cleanlab.segmentation.rank import get_label_quality_scores, issues_from_scores \n", + "from cleanlab.segmentation.summary import display_issues, common_label_issues, filter_by_class \n", + "np.set_printoptions(suppress=True)" + ] + }, + { + "attachments": { + "image-2.png": { + "image/png": "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" + } + }, + "cell_type": "markdown", + "id": "9ad75b45", + "metadata": {}, + "source": [ + "## 2. Get data, labels, and pred_probs\n", + "\n", + "We begin by loading the `labels` and `pred_probs` for our dataset, which are the only inputs required to find label issues and obtain the label quality score for each image with cleanlab. Here we use `pred_probs` which are from the SYNTHIA dataset. Our example [training notebook](https://github.com/cleanlab/examples/blob/master/segmentation/training_ResNeXt50_for_Semantic_Segmentation_on_SYNTHIA.ipynb) contains the code to produce such `pred_probs` and save them in a `.npy` file, which we simply load here via a `np.load` function.\n", + "\n", + "Here's what an example image looks like in the SYNTHIA dataset. For every image there is also `label` mask provided in which each pixel is integer-encoded as one of the SYNTHIA classes: sky, building, road, sidewalk, fence, vegetation, pole, car, traffic sign, person, bicycle, motorcycle, traffic light, terrain, rider, truck, bus, train, wall, and unlabeled. \n", + "\n", + "![image-2.png](attachment:image-2.png)" + ] + }, + { + "cell_type": "markdown", + "id": "dc888c2a", + "metadata": {}, + "source": [ + "In semantic segmentation tasks `labels` and `pred_probs` are formatted with the following dimensions:\n", + "\n", + " N - Number of images in the dataset\n", + " K - Number of classes in the dataset\n", + " H - Height of each image\n", + " W - Width of each image\n", + "\n", + "Each pixel in the dataset is labeled with one of *K* possible classes. The `pred_probs` contain a length-*K* vector for **each** pixel in the dataset (which sums to 1 for each pixel). This results in an array of size `(N,K,H,W)`. \n", + "\n", + "Note that cleanlab requires **only** `pred_probs` from any trained segmentation model and `labels` in order to detect label errors. The `pred_probs` should be **out-of-sample**, which can be obtained for every image in a dataset via K-fold cross-validation." + ] + }, + { + "cell_type": "markdown", + "id": "6c2202be", + "metadata": {}, + "source": [ + "**pred_probs**\n", + "dim: (N,K,H,W)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "07dc5678", + "metadata": {}, + "outputs": [], + "source": [ + "pred_probs_filepaths ='predicted_masks.npy'\n", + "pred_probs = np.load(pred_probs_filepaths, mmap_mode='r+')\n", + "print(pred_probs.shape)" + ] + }, + { + "cell_type": "markdown", + "id": "f2eff12e", + "metadata": {}, + "source": [ + "The `labels` contain a class label for each pixel in each image, which must be an integer in `0, 1, ..., K-1`. This results in an array of size `(N,H,W)`." + ] + }, + { + "cell_type": "markdown", + "id": "1e625c33", + "metadata": {}, + "source": [ + "**labels**\n", + "dim: (N,H,W)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25ebe22a", + "metadata": {}, + "outputs": [], + "source": [ + "label_filepaths ='given_masks.npy'\n", + "labels = np.load(label_filepaths, mmap_mode='r+')\n", + "print(labels.shape)" + ] + }, + { + "cell_type": "markdown", + "id": "9b71eb4a", + "metadata": {}, + "source": [ + "Note that these correspond to the labeled mask from the dataset, and the extracted probabilities of a trained classifier. If using your own dataset, which may consider iterating on memmaped numpy arrays.\n", + "\n", + "- `labels`: Array of dimension (N,H,W) where N is the number of images, K is the number of classes, and H and W are dimension of the image. We assume an integer encoded image. For one-hot encoding one can `np.argmax(labels_one_hot,axis=1)` assuming that `labels_one_hot` is of dimension (N,K,H,W)\n", + "- `pred_probs`: Array of dimension (N,K,H,W), similar to `labels` where `K` is the number of classes.\n", + "\n", + "**class_names**\n", + "dim: (K,)\n", + "\n", + "Some of our functions optionally use the class names to improve visualization. Here are the class names in our dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3faedea9", + "metadata": {}, + "outputs": [], + "source": [ + "SYNTHIA_CLASSES = ['unlabeled','sky', 'building', 'road', 'sidewalk', 'fence', 'vegetation','pole','car', \\\n", + " 'traffic sign','person','bicycle','motorcycle','traffic light', 'terrain', \\\n", + " 'rider', 'truck', 'bus', 'train','wall']" + ] + }, + { + "attachments": { + "synthia_errors-2.png": { + "image/png": "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" + } + }, + "cell_type": "markdown", + "id": "1dc3150f", + "metadata": {}, + "source": [ + "## 3. Use cleanlab to find label issues \n", + "\n", + "In segmentation, we consider an image mislabeled if the given mask does not match what truly appears in the image that is being segmented. More specifically, when a pixel is labeled as class `i` but the pixel _really_ belongs to class `j`. This generally happens when an image is annotated maunally by human annotators.\n", + "\n", + "Below are examples of three types of annotation errors common in segmentation datasets.\n", + "\n", + "![synthia_errors-2.png](attachment:synthia_errors-2.png)\n", + "\n", + "\n", + "Based on the given `labels` and out-of-sample `pred_probs`, cleanlab can quickly help us identify such label issues in our dataset by calling `find_label_issues()`. \n", + "\n", + "By default, the indices of the identified label issues are sorted by cleanlab’s self-confidence score, which measures the quality of each given label via the probability assigned to it by our trained model. The returned `issues` is a boolean mask of dimension `(N,H,W)`, where `True` corresponds to a detected error sorted by image quality with the lowest-quality images coming first." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c2ad9ad", + "metadata": {}, + "outputs": [], + "source": [ + "issues = find_label_issues(labels, pred_probs,downsample = 16, n_jobs=None, batch_size=100000)" + ] + }, + { + "cell_type": "markdown", + "id": "e8d9840b", + "metadata": {}, + "source": [ + "**Note:**\n", + " - The ``downsample`` flag gives us compute benefits to scale to large datasets, but for maximum label error detection accuracy, keep this value low.\n", + " - To maximize compute efficiency, try to use the largest `batch_size` your system memory allows.\n", + "\n", + "### Visualize top label issues\n", + "\n", + "Let's look at the top 2 images that cleanlab thinks are most likely mislabeled, namely images located at index 131 and 29. The part of image highlighted in red is where cleanlab believes the given mask does not match what really appears in the image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95dc7268", + "metadata": {}, + "outputs": [], + "source": [ + "display_issues(issues,top=2)" + ] + }, + { + "cell_type": "markdown", + "id": "717b3b7d", + "metadata": {}, + "source": [ + "We can also input `pred_probs`, `labels`, and `class_names` as auxiliary inputs to see more information." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57fed473", + "metadata": {}, + "outputs": [], + "source": [ + "display_issues(issues, labels=labels, pred_probs=pred_probs, class_names=SYNTHIA_CLASSES,top=2)" + ] + }, + { + "cell_type": "markdown", + "id": "116fff37", + "metadata": {}, + "source": [ + "After additionally inputting `pred_probs`, `labels`, and `class_names` we see more information:\n", + " - Inputs `labels` and `pred_probs` generates the first two columns. This segments the image based on the class that appears in the given label and what class the model predicted for those pixels.\n", + " - Input `class_names` creates the legend that color codes our segmentation.\n", + "\n", + "\n", + "In the leftmost plot we can see that the dark brown area (the `unlabeled` class as shown in the legend) was the given label. The middle plot shows our model believes that this area is infact the `sky`, a light brown shade in the legend. The rightmost plot highlights the discrepancy between these classes in red to indicate which area of the image is likely mislabeled.\n", + "\n", + "These plots clearly highlight the part of the sky that was mislabeled by annotators of this image." + ] + }, + { + "cell_type": "markdown", + "id": "d213b2b2", + "metadata": {}, + "source": [ + "### Classes which are commonly mislabeled overall \n", + "\n", + "We may also wish to understand which classes tend to be most commonly mislabeled throughout the entire dataset by calling `common_label_issues()`. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e4a006bd", + "metadata": {}, + "outputs": [], + "source": [ + "common_label_issues(issues, labels=labels, pred_probs=pred_probs, class_names=SYNTHIA_CLASSES)" + ] + }, + { + "cell_type": "markdown", + "id": "a35ef843", + "metadata": {}, + "source": [ + "The printed information above is also stored in a returned pandas DataFrame, which summarizes which classes are overall least reliably labeled in the dataset.\n", + "\n", + "### Focusing on one specific class\n", + "\n", + "We can also just focus on issues within a specific class of interest, say just the class `car`. Easily do so using `filter_by_class` to only look at the estimated label errors in the `car` class. \n", + "Here the color-coding reveals that the pixels depicting a car in the image were mistakenly left as the `unlabeled` class in the given label." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8f4e163", + "metadata": {}, + "outputs": [], + "source": [ + "class_issues = filter_by_class(SYNTHIA_CLASSES.index(\"car\"), issues,labels=labels, pred_probs=pred_probs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "716c74f3", + "metadata": {}, + "outputs": [], + "source": [ + "display_issues(class_issues, pred_probs=pred_probs, labels=labels, top=3, class_names=SYNTHIA_CLASSES)" + ] + }, + { + "cell_type": "markdown", + "id": "1759108b", + "metadata": {}, + "source": [ + "### Get label quality scores\n", + "\n", + "Cleanlab can provide an overall label quality score for each image to estimate our confidence that it is correctly labeled. These scores range from 0 to 1, such that lower scores indicate images more likely to contain some mislabeled pixels.\n", + "\n", + "**Note:** To automatically estimate *which* pixels are mislabeled (and the number of label errors) rather than ranking the images, use `find_label_issues()` instead. \n", + "\n", + "The label quality scores are most useful if you only have time to review a limited number of images and want to prioritize which ones to look at, or if you're specifically aiming to detect label errors with high precision (or high recall) rather than overall estimation of the set of mislabeled images and pixels." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db0b5179", + "metadata": {}, + "outputs": [], + "source": [ + "image_scores, pixel_scores = get_label_quality_scores(labels, pred_probs)" + ] + }, + { + "cell_type": "markdown", + "id": "d3586219", + "metadata": {}, + "source": [ + "Beyond scoring the overall label quality of each image, the above method produces a (0 to 1) quality score for each pixel. We can apply a thresholding function to these scores in order to extract the same style `True` or `False` mask as `find_label_issues()`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "390780a1", + "metadata": {}, + "outputs": [], + "source": [ + "issues_from_score = issues_from_scores(image_scores, pixel_scores, threshold=0.5) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "933d6ef0", + "metadata": {}, + "outputs": [], + "source": [ + "display_issues(issues_from_score, pred_probs=pred_probs, labels=labels, top=5) " + ] + }, + { + "cell_type": "markdown", + "id": "eacdd73d", + "metadata": {}, + "source": [ + "We can see that the errors are dominated by label errors in the sky." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86bac686", + "metadata": {}, + "outputs": [], + "source": [ + "# Note: This cell is only for docs.cleanlab.ai, if running on local Jupyter or Colab, please ignore it.\n", + "top_2_issues = np.argsort(-np.sum(issues, axis=(1, 2)))[:2]\n", + "assert (top_2_issues == [131, 29]).all()\n", + "\n", + "top_3_class_issues = np.argsort(-np.sum(class_issues, axis=(1, 2)))[:3]\n", + "assert (top_3_class_issues == [134, 66, 109]).all()\n", + "\n", + "highlighted_indices = [ 78, 16, 120, 34, 137, 51, 71, 8, 106, 94]\n", + "if not all(np.argsort(issues_from_score.sum((1,2)))[:10]==highlighted_indices):\n", + " raise Exception(\"Some highlighted examples are missing from ranked_label_issues.\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/tutorials/tabular.ipynb b/docs/source/tutorials/tabular.ipynb index 94e577c9fe..2841867c1e 100644 --- a/docs/source/tutorials/tabular.ipynb +++ b/docs/source/tutorials/tabular.ipynb @@ -8,20 +8,32 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "In this 5-minute quickstart tutorial, we use cleanlab with scikit-learn models to find potential label errors in a classification dataset with tabular (numeric/categorical) features. Here we study the [German Credit](https://www.openml.org/d/31) dataset which contains 1,000 individuals described by 20 features, each labeled as either \"good\" or \"bad\" credit risk. cleanlab automatically shortlists _hundreds_ of examples from this dataset that confuse our ML model; many of which are potential label errors (due to annotator mistakes), edge cases, and otherwise ambiguous examples.\n", + "
\n", + "Consider Using Datalab\n", + "
\n", "\n", - "**Overview of what we'll do in this tutorial:**\n", + "If you are just interested in detecting a wide variety of issues in these datasets, check out the [Datalab version of this tabular tutorial](https://docs.cleanlab.ai/stable/tutorials/datalab/tabular.html).\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this 5-minute quickstart tutorial, we use cleanlab with scikit-learn models to find potential label errors in a classification dataset with tabular (numeric/categorical) features. Tabular (or *structured*) data are typically organized in a row/column format and stored in a SQL database or file types like: CSV, Excel, or Parquet. Here we consider a Student Grades dataset, which contains over 900 individuals who have three exam grades and some optional notes, each being assigned a letter grade (their class label). cleanlab automatically identifies _hundreds_ of examples in this dataset that were mislabeled with the incorrect final grade selected. This tutorial will teach you how to use this package to detect incorrect labels in your own tabular datasets.\n", "\n", - "- Build a simple credit risk classifier with scikit-learn's HistGradientBoostingClassifier.\n", "\n", - "- Use this classifier to compute out-of-sample predicted probabilities, `pred_probs`, via cross validation.\n", + "**Overview of what we'll do in this tutorial:**\n", + "\n", + "- Train a classifier model (here scikit-learn's ExtraTreesClassifier, although any model could be used) and use this classifier to compute (out-of-sample) predicted class probabilities via cross-validation.\n", "\n", "- Identify potential label errors in the data with cleanlab's `find_label_issues` method.\n", "\n", - "- Train a robust version of the same histogram-based gradient boosting model via cleanlab's `CleanLearning` wrapper.\n" + "- Train a robust version of the same ExtraTrees model via cleanlab's `CleanLearning` wrapper.\n" ] }, { @@ -133,6 +145,14 @@ "source": [ "import random\n", "import numpy as np\n", + "import pandas as pd \n", + "from sklearn.preprocessing import StandardScaler, LabelEncoder\n", + "from sklearn.model_selection import cross_val_predict, train_test_split\n", + "from sklearn.metrics import accuracy_score\n", + "from sklearn.ensemble import ExtraTreesClassifier\n", + "\n", + "from cleanlab.filter import find_label_issues\n", + "from cleanlab.classification import CleanLearning\n", "\n", "SEED = 100\n", "\n", @@ -151,7 +171,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We first load the data features and labels.\n" + "We first load the data features and labels (which are possibly noisy).\n" ] }, { @@ -160,18 +180,25 @@ "metadata": {}, "outputs": [], "source": [ - "from sklearn.datasets import fetch_openml\n", - "\n", - "data = fetch_openml(\"credit-g\", version=1) # get the credit data from OpenML\n", - "X_raw = data.data # features (pandas DataFrame)\n", - "labels_raw = data.target # labels (pandas Series)" + "grades_data = pd.read_csv(\"https://s.cleanlab.ai/grades-tabular-demo-v2.csv\")\n", + "grades_data.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "X_raw = grades_data[[\"exam_1\", \"exam_2\", \"exam_3\", \"notes\"]]\n", + "labels_raw = grades_data[\"letter_grade\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Next we preprocess the data. Here we apply one-hot encoding to features with categorical data, and standardize features with numeric data. We also perform label encoding on the labels --- \"bad\" is encoded as 0 and \"good\" is encoded as 1." + "Next we preprocess the data. Here we apply one-hot encoding to features with categorical data, and standardize features with numeric data. We also perform label encoding on the labels, as cleanlab's functions require the labels for each example to be an interger integer in 0, 1, …, num_classes - 1. " ] }, { @@ -180,18 +207,17 @@ "metadata": {}, "outputs": [], "source": [ - "import pandas as pd\n", - "from sklearn.preprocessing import StandardScaler\n", - "\n", - "cat_features = X_raw.select_dtypes(\"category\").columns\n", - "X_encoded = pd.get_dummies(X_raw, columns=cat_features, drop_first=True)\n", + "categorical_features = [\"notes\"]\n", + "X_encoded = pd.get_dummies(X_raw, columns=categorical_features, drop_first=True)\n", "\n", - "num_features = X_raw.select_dtypes(\"float64\").columns\n", + "numeric_features = [\"exam_1\", \"exam_2\", \"exam_3\"]\n", "scaler = StandardScaler()\n", - "X_scaled = X_encoded.copy()\n", - "X_scaled[num_features] = scaler.fit_transform(X_encoded[num_features])\n", + "X_processed = X_encoded.copy()\n", + "X_processed[numeric_features] = scaler.fit_transform(X_encoded[numeric_features])\n", "\n", - "labels = labels_raw.map({\"bad\": 0, \"good\": 1}) # encode labels as integers" + "encoder = LabelEncoder()\n", + "encoder.fit(labels_raw)\n", + "labels = encoder.transform(labels_raw)" ] }, { @@ -220,7 +246,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Here we use a simple histogram-based gradient boosting model (similar to XGBoost), but you can choose any suitable scikit-learn model for this tutorial.\n" + "Here we use a simple ExtraTrees classifier that fits various randomized decision tress on our data, but you can choose any suitable scikit-learn model for this tutorial." ] }, { @@ -229,18 +255,16 @@ "metadata": {}, "outputs": [], "source": [ - "from sklearn.ensemble import HistGradientBoostingClassifier\n", - "\n", - "clf = HistGradientBoostingClassifier()" + "clf = ExtraTreesClassifier()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "To identify label issues, cleanlab requires a probabilistic prediction from your model for every datapoint. However, these predictions will be _overfitted_ (and thus unreliable) for examples the model was previously trained on. cleanlab is intended to only be used with **out-of-sample** predicted probabilities, i.e., on examples held out from the model during the training.\n", + "To find potential labeling errors, cleanlab requires a probabilistic prediction from your model for every datapoint. However, these predictions will be _overfitted_ (and thus unreliable) for examples the model was previously trained on. For the best results, cleanlab should be applied with **out-of-sample** predicted class probabilities, i.e., on examples held out from the model during the training.\n", "\n", - "K-fold cross-validation is a straightforward way to produce out-of-sample predicted probabilities for every datapoint in the dataset by training K copies of our model on different data subsets and using each copy to predict on the subset of data it did not see during training. An additional benefit of cross-validation is that it provides a more reliable evaluation of our model than a single training/validation split. We can obtain cross-validated out-of-sample predicted probabilities from any classifier via a simple scikit-learn wrapper:\n" + "K-fold cross-validation is a straightforward way to produce out-of-sample predicted probabilities for every datapoint in the dataset by training K copies of our model on different data subsets and using each copy to predict on the subset of data it did not see during training. An additional benefit of cross-validation is that it provides a more reliable evaluation of our model than a single training/validation split. We can implement this via the `cross_val_predict` method from scikit-learn:\n" ] }, { @@ -249,12 +273,10 @@ "metadata": {}, "outputs": [], "source": [ - "from sklearn.model_selection import cross_val_predict\n", - "\n", - "num_crossval_folds = 3 # for efficiency; values like 5 or 10 will generally work better\n", + "num_crossval_folds = 5 \n", "pred_probs = cross_val_predict(\n", " clf,\n", - " X_scaled,\n", + " X_processed,\n", " labels,\n", " cv=num_crossval_folds,\n", " method=\"predict_proba\",\n", @@ -272,7 +294,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Based on the given labels and out-of-sample predicted probabilities, cleanlab can quickly help us identify label issues in our dataset. For a dataset with N examples from K classes, the labels should be a 1D array of length N and predicted probabilities should be a 2D (N x K) array. Here we request that the indices of the identified label issues be sorted by cleanlab's self-confidence score, which measures the quality of each given label via the probability assigned to it in our model's prediction." + "Based on the given labels and out-of-sample predicted probabilities, cleanlab can quickly help us identify poorly labeled instances in our data table. For a dataset with N examples from K classes, the labels should be a 1D array of length N and predicted probabilities should be a 2D (N x K) array. Here we request that the indices of the identified label issues be sorted by cleanlab's self-confidence score, which measures the quality of each given label via the probability assigned to it in our model's prediction." ] }, { @@ -281,8 +303,6 @@ "metadata": {}, "outputs": [], "source": [ - "from cleanlab.filter import find_label_issues\n", - "\n", "ranked_label_issues = find_label_issues(\n", " labels=labels, pred_probs=pred_probs, return_indices_ranked_by=\"self_confidence\"\n", ")\n", @@ -310,7 +330,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "These examples appear the most suspicious to our model and should be carefully re-examined. Perhaps the original annotators missed something when deciding on the labels for these individuals.\n" + "These examples have been labeled incorrectly and should be carefully re-examined - a student with grades of 99, 86 and 74 surely does not deserve a D, and a student who cheated on their exam probably does not deserve an A either! \n", + "\n", + "This is a straightforward approach to visualize the rows in a data table that might be mislabeled." ] }, { @@ -333,12 +355,10 @@ "metadata": {}, "outputs": [], "source": [ - "from sklearn.model_selection import train_test_split\n", - "\n", "X_train, X_test, labels_train, labels_test = train_test_split(\n", " X_encoded,\n", " labels,\n", - " test_size=0.25,\n", + " test_size=0.2,\n", " random_state=SEED,\n", ")" ] @@ -357,20 +377,15 @@ "outputs": [], "source": [ "scaler = StandardScaler()\n", - "X_train[num_features] = scaler.fit_transform(X_train[num_features])\n", - "X_test[num_features] = scaler.transform(X_test[num_features])\n", - "\n", - "X_train = X_train.to_numpy()\n", - "labels_train = labels_train.to_numpy()\n", - "X_test = X_test.to_numpy()\n", - "labels_test = labels_test.to_numpy()" + "X_train[numeric_features] = scaler.fit_transform(X_train[numeric_features])\n", + "X_test[numeric_features] = scaler.transform(X_test[numeric_features])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's now train and evaluate the original gradient boosting model.\n" + "Let's now train and evaluate the original ExtraTrees model.\n" ] }, { @@ -379,8 +394,6 @@ "metadata": {}, "outputs": [], "source": [ - "from sklearn.metrics import accuracy_score\n", - "\n", "clf.fit(X_train, labels_train)\n", "acc_og = clf.score(X_test, labels_test)\n", "print(f\"Test accuracy of original model: {acc_og}\")" @@ -399,9 +412,7 @@ "metadata": {}, "outputs": [], "source": [ - "from cleanlab.classification import CleanLearning\n", - "\n", - "clf = HistGradientBoostingClassifier() # Note we first re-initialize clf\n", + "clf = ExtraTreesClassifier() # Note we first re-initialize clf\n", "cl = CleanLearning(clf) # cl has same methods/attributes as clf" ] }, @@ -456,6 +467,10 @@ "source": [ "# Note: This cell is only for docs.cleanlab.ai, if running on local Jupyter or Colab, please ignore it.\n", "\n", + "highlighted_indices = [827, 637] # check these examples were top 5 in find_label_issues\n", + "if not all(x in ranked_label_issues[:5] for x in highlighted_indices):\n", + " raise Exception(\"Some highlighted examples are missing from ranked_label_issues.\")\n", + "\n", "if acc_og >= acc_cl: # check cleanlab has improved prediction accuracy\n", " raise Exception(\"Cleanlab training failed to improve model accuracy.\")" ] @@ -480,7 +495,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.10.8" } }, "nbformat": 4, diff --git a/docs/source/tutorials/text.ipynb b/docs/source/tutorials/text.ipynb index 3d39c1a936..27d1756dcf 100644 --- a/docs/source/tutorials/text.ipynb +++ b/docs/source/tutorials/text.ipynb @@ -8,20 +8,33 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "In this 5-minute quickstart tutorial, we use cleanlab to find potential label errors in a text classification dataset of [IMDb movie reviews](https://ai.stanford.edu/~amaas/data/sentiment/). This dataset contains 50,000 text reviews, each labeled with a binary sentiment polarity label indicating whether the review is positive (1) or negative (0). cleanlab will shortlist _hundreds_ of examples that confuse our ML model the most; many of which are potential label errors, edge cases, or otherwise ambiguous examples.\n", + "
\n", + "Consider Using Datalab\n", + "
\n", + "\n", + "If you are just interested in detecting a wide variety of issues in these datasets, check out the [Datalab version of this text tutorial](https://docs.cleanlab.ai/stable/tutorials/datalab/text.html).\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this 5-minute quickstart tutorial, we use cleanlab to find potential label errors in an intent classification dataset composed of (text) customer service requests at an online bank. We consider a subset of the [Banking77-OOS Dataset](https://arxiv.org/abs/2106.04564) containing 1,000 customer service requests which can be classified into 10 categories corresponding to the intent of the request. cleanlab will shortlist examples that confuse our ML model the most; many of which are potential label errors, out-of-scope examples, or otherwise ambiguous examples.\n", "\n", "**Overview of what we'll do in this tutorial:**\n", "\n", - "- Build a simple TensorFlow & Keras neural net and wrap it with [SciKeras](https://www.adriangb.com/scikeras/) to make it scikit-learn compatible.\n", + "- Use a pretrained transformer model to extract the text embeddings from the customer service requests.\n", "\n", - "- Use this classifier to compute out-of-sample predicted probabilities, `pred_probs`, via cross validation.\n", + "- Train a simple Logistic Regression model on the text embeddings to compute out-of-sample predicted probabilities.\n", "\n", - "- Identify potential label errors in the data with cleanlab's `find_label_issues` method.\n", + "- Use `CleanLearning` to automatically compute out-of-sample preddicted probabilites and identify potential label errors with the `find_label_issues` method.\n", "\n", - "- Train a more robust version of the same neural net via cleanlab's `CleanLearning` wrapper.\n" + "- Train a more robust version of the same model after dropping the identified label errors using `CleanLearning`." ] }, { @@ -32,7 +45,9 @@ "Quickstart\n", "
\n", " \n", - "Already have an sklearn compatible `model`, text `data` and given `labels`? Run the code below to train your `model` and get label issues.\n", + "Already have an sklearn compatible `model`, `data` and given `labels`? Run the code below to train your `model` and get label issues using `CleanLearning`. \n", + " \n", + "You can subsequently use the same `CleanLearning` object to train a more robust model (only trained on the clean data) by calling the `.fit()` method and passing in the `label_issues` found earlier.\n", "\n", "\n", "
\n", @@ -42,10 +57,11 @@ "from cleanlab.classification import CleanLearning\n", "\n", "cl = CleanLearning(model)\n", - "_ = cl.fit(train_data, labels)\n", - "label_issues = cl.get_label_issues()\n", - "preds = cl.predict(test_data) # predictions from a version of your model \n", - " # trained on auto-cleaned data\n", + "label_issues = cl.find_label_issues(train_data, labels) # identify mislabeled examples \n", + " \n", + "cl.fit(train_data, labels, label_issues=label_issues)\n", + "preds = cl.predict(test_data) # predictions from a version of your model \n", + " # trained on auto-cleaned data\n", "\n", "\n", "```\n", @@ -88,7 +104,7 @@ "You can use `pip` to install all packages required for this tutorial as follows:\n", "\n", "```ipython3\n", - "!pip install sklearn tensorflow tensorflow-datasets scikeras\n", + "!pip install sklearn sentence-transformers\n", "!pip install cleanlab\n", "# Make sure to install the version corresponding to this tutorial\n", "# E.g. if viewing master branch documentation:\n", @@ -106,15 +122,13 @@ "source": [ "# Package installation (hidden on docs.cleanlab.ai).\n", "# If running on Colab, may want to use GPU (select: Runtime > Change runtime type > Hardware accelerator > GPU)\n", - "# Package versions we used: tensorflow==2.9.1 scikeras==0.9.0 scikit-learn==1.1.3 tensorflow_datasets==4.5.2\n", + "# Package versions we used:scikit-learn==1.2.0 sentence-transformers==2.2.2\n", "\n", - "dependencies = [\"cleanlab\", \"sklearn\", \"tensorflow\", \"tensorflow_datasets\", \"scikeras\"]\n", + "dependencies = [\"cleanlab\", \"sklearn\", \"sentence_transformers\"]\n", "\n", "# Supress outputs that may appear if tensorflow happens to be improperly installed: \n", "import os \n", - "import logging \n", - "os.environ[\"TF_CPP_MIN_LOG_LEVEL\"] = \"3\" # suppress tensorflow log output \n", - "logging.getLogger('tensorflow').setLevel(logging.FATAL) \n", + "os.environ[\"TOKENIZERS_PARALLELISM\"] = \"false\" # disable parallelism to avoid deadlocks with huggingface\n", "\n", "if \"google.colab\" in str(get_ipython()): # Check if it's running in Google Colab\n", " %pip install cleanlab # for colab\n", @@ -143,14 +157,13 @@ "import re \n", "import string \n", "import pandas as pd \n", - "from sklearn.metrics import accuracy_score, log_loss \n", - "from sklearn.model_selection import cross_val_predict \n", - "import tensorflow as tf \n", - "from tensorflow.keras import layers \n", - "import tensorflow_datasets as tfds \n", - "from scikeras.wrappers import KerasClassifier \n", + "from sklearn.metrics import accuracy_score\n", + "from sklearn.model_selection import train_test_split, cross_val_predict \n", + "from sklearn.preprocessing import LabelEncoder\n", + "from sklearn.linear_model import LogisticRegression\n", + "from sentence_transformers import SentenceTransformer\n", "\n", - "SEED = 123456 # for reproducibility " + "from cleanlab.classification import CleanLearning" ] }, { @@ -168,7 +181,8 @@ "\n", "pd.set_option(\"display.max_colwidth\", None) \n", "\n", - "tf.keras.utils.set_random_seed(SEED)\n", + "SEED = 123456 # for reproducibility \n", + "\n", "np.random.seed(SEED)\n", "random.seed(SEED)" ] @@ -177,14 +191,17 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 2. Load and preprocess the IMDb text dataset\n" + "## 2. Load and format the text dataset\n" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "This dataset is provided in TensorFlow's Datasets.\n" + "data = pd.read_csv(\"https://s.cleanlab.ai/banking-intent-classification.csv\")\n", + "data.head()" ] }, { @@ -193,12 +210,9 @@ "metadata": {}, "outputs": [], "source": [ - "%%capture\n", + "raw_texts, raw_labels = data[\"text\"].values, data[\"label\"].values\n", "\n", - "raw_full_ds = tfds.load(\n", - " name=\"imdb_reviews\", split=(\"train+test\"), batch_size=-1, as_supervised=True\n", - ")\n", - "raw_full_texts, full_labels = tfds.as_numpy(raw_full_ds)" + "raw_train_texts, raw_test_texts, raw_train_labels, raw_test_labels = train_test_split(raw_texts, raw_labels, test_size=0.1)" ] }, { @@ -207,15 +221,17 @@ "metadata": {}, "outputs": [], "source": [ - "num_classes = len(set(full_labels))\n", - "print(f\"Classes: {set(full_labels)}\")" + "num_classes = len(set(raw_train_labels))\n", + "\n", + "print(f\"This dataset has {num_classes} classes.\")\n", + "print(f\"Classes: {set(raw_train_labels)}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's print the first example." + "Let's print the first example in the train set." ] }, { @@ -225,44 +241,25 @@ "outputs": [], "source": [ "i = 0\n", - "print(f\"Example Label: {full_labels[i]}\")\n", - "print(f\"Example Text: {raw_full_texts[i]}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The data are stored as two numpy arrays:\n", - "\n", - "1. `raw_full_texts` for the movie reviews in text format,\n", - "2. `full_labels` for the labels.\n" + "print(f\"Example Label: {raw_train_labels[i]}\")\n", + "print(f\"Example Text: {raw_train_texts[i]}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "
\n", - "Bringing Your Own Data (BYOD)?\n", + "The data is stored as two numpy arrays for each the train and test set:\n", "\n", - "You can easily replace the above with your own text dataset, and continue with the rest of the tutorial.\n", - "\n", - "Your classes (and entries of `full_labels`) should be represented as integer indices 0, 1, ..., num_classes - 1.\n", - "For example, if your dataset has 7 examples from 3 classes, `full_labels` might be: `np.array([2,0,0,1,2,0,1])`\n", - "\n", - "
\n" + "1. `raw_train_texts` and `raw_test_texts` store the customer service requests utterances in text format\n", + "2. `raw_train_labels` and `raw_test_labels` store the intent categories (labels) for each example\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Define a function to preprocess the text data by:\n", - "\n", - "1. Converting it to lower case\n", - "2. Removing the HTML break tags: `
`\n", - "3. Removing any punctuation marks\n" + "First, we need to perform label enconding on the labels, cleanlab's functions require the labels for each example to be an interger integer in 0, 1, …, num_classes - 1. We will use sklearn's `LabelEncoder` to encode our labels.\n" ] }, { @@ -271,41 +268,35 @@ "metadata": {}, "outputs": [], "source": [ - "def preprocess_text(input_data):\n", - " lowercase = tf.strings.lower(input_data)\n", - " stripped_html = tf.strings.regex_replace(lowercase, \"
\", \" \")\n", - " return tf.strings.regex_replace(stripped_html, f\"[{re.escape(string.punctuation)}]\", \"\")" + "encoder = LabelEncoder()\n", + "encoder.fit(raw_train_labels)\n", + "\n", + "train_labels = encoder.transform(raw_train_labels)\n", + "test_labels = encoder.transform(raw_test_labels)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We use a `TextVectorization` layer to preprocess, tokenize, and vectorize our text data, thus making it suitable as input for a neural network.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "max_features = 10000\n", - "sequence_length = 250\n", + "
\n", + "Bringing Your Own Data (BYOD)?\n", "\n", - "vectorize_layer = layers.TextVectorization(\n", - " standardize=preprocess_text,\n", - " max_tokens=max_features,\n", - " output_mode=\"int\",\n", - " output_sequence_length=sequence_length,\n", - ")" + "You can easily replace the above with your own text dataset, and continue with the rest of the tutorial.\n", + "\n", + "Your classes (and entries of `train_labels` / `test_labels`) should be represented as integer indices 0, 1, ..., num_classes - 1.\n", + "For example, if your dataset has 7 examples from 3 classes, `train_labels` might be: `np.array([2,0,0,1,2,0,1])`\n", + "\n", + "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Adapting `vectorize_layer` to the text data creates a mapping of each token (i.e. word) to an integer index. Subsequently, we can vectorize our text data by using this mapping. Finally, we'll also convert our text data into a numpy array as required by cleanlab.\n" + "Next we convert the text strings into vectors better suited as inputs for our ML model. \n", + "\n", + "We will use numeric representations from a pretrained Transformer model as embeddings of our text. The [Sentence Transformers](https://huggingface.co/docs/hub/sentence-transformers) library offers simple methods to compute these embeddings for text data. Here, we load the pretrained `electra-small-discriminator` model, and then run our data through network to extract a vector embedding of each example." ] }, { @@ -314,59 +305,33 @@ "metadata": {}, "outputs": [], "source": [ - "%%capture\n", + "transformer = SentenceTransformer('google/electra-small-discriminator')\n", "\n", - "vectorize_layer.adapt(raw_full_texts)\n", - "full_texts = vectorize_layer(raw_full_texts)\n", - "full_texts = full_texts.numpy()" + "train_texts = transformer.encode(raw_train_texts)\n", + "test_texts = transformer.encode(raw_test_texts)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## 3. Define a classification model and compute out-of-sample predicted probabilities\n" + "Our subsequent ML model will directly operate on elements of `train_texts` and `test_texts` in order to classify the customer service requests." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Here, we build a simple neural network for classification with TensorFlow and Keras.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def get_net():\n", - " net = tf.keras.Sequential(\n", - " [\n", - " tf.keras.Input(shape=(None,), dtype=\"int64\"),\n", - " layers.Embedding(max_features + 1, 16),\n", - " layers.Dropout(0.2),\n", - " layers.GlobalAveragePooling1D(),\n", - " layers.Dropout(0.2),\n", - " layers.Dense(num_classes),\n", - " layers.Softmax()\n", - " ]\n", - " ) # outputs probability that text belongs to class 1\n", + "## 3. Define a classification model and use cleanlab to find potential label errors\n", "\n", - " net.compile(\n", - " optimizer=\"adam\",\n", - " loss=tf.keras.losses.SparseCategoricalCrossentropy(),\n", - " metrics=tf.keras.metrics.CategoricalAccuracy(),\n", - " )\n", - " return net" + "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "As some of cleanlab's feature requires scikit-learn compatibility, we will need to adapt the above TensorFlow & Keras neural net accordingly. [SciKeras](https://www.adriangb.com/scikeras/stable/) is a convenient package that makes this really easy.\n" + "A typical way to leverage pretrained networks for a particular classification task is to add a linear output layer and fine-tune the network parameters on the new data. However this can be computationally intensive. Alternatively, we can freeze the pretrained weights of the network and only train the output layer without having to rely on GPU(s). Here we do this conveniently by fitting a scikit-learn linear model on top of the extracted embeddings." ] }, { @@ -375,16 +340,16 @@ "metadata": {}, "outputs": [], "source": [ - "model = KerasClassifier(get_net(), epochs=10)" + "model = LogisticRegression(max_iter=400)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "To identify label issues, cleanlab requires a probabilistic prediction from your model for every datapoint that should be considered. However these predictions will be _overfit_ (and thus unreliable) for datapoints the model was previously trained on. cleanlab is intended to only be used with **out-of-sample** predicted probabilities, i.e. on datapoints held-out from the model during the training.\n", + "We can define the `CleanLearning` object with our Logistic Regression model and use `find_label_issues` to identify potential label errors.\n", "\n", - "K-fold cross-validation is a straightforward way to produce out-of-sample predicted probabilities for every datapoint in the dataset, by training K copies of our model on different data subsets and using each copy to predict on the subset of data it did not see during training. We can obtain cross-validated out-of-sample predicted probabilities from any classifier via a scikit-learn simple wrapper:\n" + "`CleanLearning` provides a wrapper class that can easily be applied to any scikit-learn compatible model, which can be used to find potential label issues and train a more robust model if the original data contains noisy labels." ] }, { @@ -393,45 +358,29 @@ "metadata": {}, "outputs": [], "source": [ - "num_crossval_folds = 3 # for efficiency; values like 5 or 10 will generally work better\n", - "pred_probs = cross_val_predict(\n", - " model,\n", - " full_texts,\n", - " full_labels,\n", - " cv=num_crossval_folds,\n", - " method=\"predict_proba\",\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "An additional benefit of cross-validation is that it facilitates more reliable evaluation of our model than a single training/validation split." + "cv_n_folds = 5 # for efficiency; values like 5 or 10 will generally work better\n", + "\n", + "cl = CleanLearning(model, cv_n_folds=cv_n_folds)" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "scrolled": true + }, "outputs": [], "source": [ - "loss = log_loss(full_labels, pred_probs) # score to evaluate probabilistic predictions, lower values are better\n", - "print(f\"Cross-validated estimate of log loss: {loss:.3f}\")" + "label_issues = cl.find_label_issues(X=train_texts, labels=train_labels)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## 4. Use cleanlab to find potential label errors\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Based on the given labels and out-of-sample predicted probabilities, cleanlab can quickly help us identify label issues in our dataset. For a dataset with N examples from K classes, the labels should be a 1D array of length N and predicted probabilities should be a 2D (N x K) array. Here we request that the indices of the identified label issues should be sorted by cleanlab's self-confidence score, which measures the quality of each given label via the probability assigned it in our model's prediction.\n" + "The `find_label_issues` method above will perform cross validation to compute out-of-sample predicted probabilites for each example, which is used to identify label issues.\n", + "\n", + "This method returns a dataframe containing a label quality score for each example. These numeric scores lie between 0 and 1, where lower scores indicate examples more likely to be mislabeled. The dataframe also contains a boolean column specifying whether or not each example is identified to have a label issue (indicating it is likely mislabeled). Note that the given and predicted labels here are encoded as intergers as that was the format expected by `cleanlab`, we will inverse transform them later in this tutorial." ] }, { @@ -440,18 +389,14 @@ "metadata": {}, "outputs": [], "source": [ - "from cleanlab.filter import find_label_issues\n", - "\n", - "ranked_label_issues = find_label_issues(\n", - " labels=full_labels, pred_probs=pred_probs, return_indices_ranked_by=\"self_confidence\"\n", - ")" + "label_issues.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's review some of the most likely label errors:\n" + "We can get the subset of examples flagged with label issues, and also sort by label quality score to find the indices of the 10 most likely mislabeled examples in our dataset." ] }, { @@ -460,17 +405,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(\n", - " f\"cleanlab found {len(ranked_label_issues)} potential label errors.\\n\"\n", - " f\"Here are indices of the top 10 most likely errors: \\n {ranked_label_issues[:10]}\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To help us inspect these datapoints, we define a method to print any example from the dataset. We then display some of the top-ranked label issues identified by `cleanlab`:\n" + "identified_issues = label_issues[label_issues[\"is_label_issue\"] == True]\n", + "lowest_quality_labels = label_issues[\"label_quality\"].argsort()[:10].to_numpy()" ] }, { @@ -479,33 +415,19 @@ "metadata": {}, "outputs": [], "source": [ - "def print_as_df(index):\n", - " return pd.DataFrame(\n", - " {\"texts\": raw_full_texts[index], \"labels\": full_labels[index]},\n", - " [index]\n", - " )" + "print(\n", + " f\"cleanlab found {len(identified_issues)} potential label errors in the dataset.\\n\"\n", + " f\"Here are indices of the top 10 most likely errors: \\n {lowest_quality_labels}\"\n", + ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Here's a review labeled as positive (1), but it should be negative (0).\n", - "Some noteworthy snippets extracted from the review text:\n", + "Let's review some of the most likely label errors. To help us inspect these datapoints, we define a method to print any example from the dataset, together with its given (original) label and the suggested alternative label from cleanlab.\n", "\n", - "> - \"...incredibly **awful** score...\"\n", - ">\n", - "> - \"...**worst** Foley work ever done.\"\n", - ">\n", - "> - \"...script is **incomprehensible**...\"\n", - ">\n", - "> - \"...editing is just **bizarre**.\"\n", - ">\n", - "> - \"...**atrocious** pan and scan...\"\n", - ">\n", - "> - \"...**incoherent mess**...\"\n", - ">\n", - "> - \"...**amateur** directing there.\"\n" + "We then display some of the top-ranked label issues identified by cleanlab:" ] }, { @@ -514,25 +436,14 @@ "metadata": {}, "outputs": [], "source": [ - "print_as_df(44582)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here's a review labeled as positive (1), but it should be negative (0).\n", - "Some noteworthy snippets extracted from the review text:\n", - "\n", - "> - \"...film seems **cheap**.\"\n", - ">\n", - "> - \"...unbelievably **bad**...\"\n", - ">\n", - "> - \"...cinematography is **badly** lit...\"\n", - ">\n", - "> - \"...everything looking **grainy** and **ugly**.\"\n", - ">\n", - "> - \"...sound is so **terrible**...\"\n" + "def print_as_df(index):\n", + " return pd.DataFrame(\n", + " {\n", + " \"text\": raw_train_texts, \n", + " \"given_label\": raw_train_labels,\n", + " \"predicted_label\": encoder.inverse_transform(label_issues[\"predicted_label\"]),\n", + " },\n", + " ).iloc[index]" ] }, { @@ -541,168 +452,79 @@ "metadata": {}, "outputs": [], "source": [ - "print_as_df(10404)" + "print_as_df(lowest_quality_labels[:5])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Here's a review labeled as positive (1), but it should be negative (0).\n", - "Some noteworthy snippets extracted from the review text:\n", + "These are very clear label errors that cleanlab has identified in this data! Note that the `given_label` does not correctly reflect the intent of these requests, whoever produced this dataset made many mistakes that are important to address before modeling the data.\n", "\n", - "> - \"...hard to imagine a **boring** shark movie...\"\n", - ">\n", - "> - \"**Poor focus** in some scenes made the production seems **amateurish**.\"\n", - ">\n", - "> - \"...**do nothing** to take advantage of...\"\n", - ">\n", - "> - \"...**far too few** scenes of any depth or variety.\"\n", - ">\n", - "> - \"...just **look flat**...no contrast of depth...\"\n", - ">\n", - "> - \"...**introspective** and **dull**...constant **disappointment**.\"\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print_as_df(30151)" + "cleanlab has shortlisted the most likely label errors to speed up your data cleaning process. With this list, you can decide whether to fix these label issues or remove ambiguous examples from the dataset." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "cleanlab has shortlisted the most likely label errors to speed up your data cleaning process. With this list, you can decide whether to fix these label issues or remove ambiguous examples from the dataset.\n" + "## 4. Train a more robust model from noisy labels\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## 5. Train a more robust model from noisy labels\n" + "Fixing the label issues manually may be time-consuming, but cleanlab can filter these noisy examples and train a model on the remaining clean data for you automatically.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Fixing the label issues manually may be time-consuming, but at least cleanlab can filter these noisy examples and train a model on the remaining clean data for you automatically.\n", - "To demonstrate this, we first reload the dataset, this time with separate train and test splits.\n" + "To establish a baseline, let's first train and evaluate our original Logistic Regression model.\n" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "scrolled": true + }, "outputs": [], "source": [ - "raw_train_ds = tfds.load(name=\"imdb_reviews\", split=\"train\", batch_size=-1, as_supervised=True)\n", - "raw_test_ds = tfds.load(name=\"imdb_reviews\", split=\"test\", batch_size=-1, as_supervised=True)\n", + "baseline_model = LogisticRegression(max_iter=400) # note we first re-instantiate the model\n", + "baseline_model.fit(X=train_texts, y=train_labels)\n", "\n", - "raw_train_texts, train_labels = tfds.as_numpy(raw_train_ds)\n", - "raw_test_texts, test_labels = tfds.as_numpy(raw_test_ds)" + "preds = baseline_model.predict(test_texts)\n", + "acc_og = accuracy_score(test_labels, preds)\n", + "print(f\"\\n Test accuracy of original model: {acc_og}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We featurize the raw text using the same `vectorize_layer` as before, but first, reset its state and adapt it only on the train set (as is proper ML practice). We finally convert the vectorized text data in the train/test sets into numpy arrays.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "vectorize_layer.reset_state()\n", - "vectorize_layer.adapt(raw_train_texts)\n", + "Now that we have a baseline, let's check if using `CleanLearning` improves our test accuracy.\n", "\n", - "train_texts = vectorize_layer(raw_train_texts)\n", - "test_texts = vectorize_layer(raw_test_texts)\n", + "`CleanLearning` provides a wrapper that can be applied to any scikit-learn compatible model. The resulting model object can be used in the same manner, but it will now train more robustly if the data has noisy labels.\n", "\n", - "train_texts = train_texts.numpy()\n", - "test_texts = test_texts.numpy()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's now train and evaluate our original neural network model.\n" + "We can use the same `CleanLearning` object defined above, and pass the label issues we already computed into `.fit()` via the `label_issues` argument. This accelerates things; if we did not provide the label issues, then they would be recomputed via cross-validation. After that `CleanLearning` simply deletes the examples with label issues and retrains your model on the remaining data." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "model = KerasClassifier(get_net(), epochs=10)\n", - "model.fit(train_texts, train_labels)\n", - "\n", - "preds = model.predict(test_texts)\n", - "acc_og = accuracy_score(test_labels, preds)\n", - "print(f\"\\n Test accuracy of original neural net: {acc_og}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "cleanlab provides a wrapper class that can easily be applied to any scikit-learn compatible model. Once wrapped, the resulting model can still be used in the exact same manner, but it will now train more robustly if the data have noisy labels.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, + "metadata": { + "scrolled": true + }, "outputs": [], "source": [ - "from cleanlab.classification import CleanLearning\n", + "cl.fit(X=train_texts, labels=train_labels, label_issues=cl.get_label_issues())\n", "\n", - "model = KerasClassifier(get_net(), epochs=10) # Note we first re-instantiate the model\n", - "cl = CleanLearning(clf=model, seed=SEED) # cl has same methods/attributes as model" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "When we train the cleanlab-wrapped model, the following operations take place: The original model is trained in a cross-validated fashion to produce out-of-sample predicted probabilities. Then, these predicted probabilities are used to identify label issues, which are then removed from the dataset. Finally, the original model is trained once more on the remaining clean subset of the data.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "_ = cl.fit(train_texts, train_labels)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can get predictions from the resulting cleanlab model and evaluate them, just like we did for our original neural network.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ "pred_labels = cl.predict(test_texts)\n", "acc_cl = accuracy_score(test_labels, pred_labels)\n", - "print(f\"Test accuracy of cleanlab's neural net: {acc_cl}\")" + "print(f\"Test accuracy of cleanlab's model: {acc_cl}\")" ] }, { @@ -722,8 +544,8 @@ "source": [ "# Note: This cell is only for docs.cleanlab.ai, if running on local Jupyter or Colab, please ignore it.\n", "\n", - "highlighted_indices = [44582, 10404, 30151] # check these examples were found in find_label_issues\n", - "if not all(x in ranked_label_issues for x in highlighted_indices):\n", + "highlighted_indices = [646, 390, 628, 702] # check these examples were found in find_label_issues\n", + "if not all(x in identified_issues.index for x in highlighted_indices):\n", " raise Exception(\"Some highlighted examples are missing from ranked_label_issues.\")\n", "\n", "# Also check that cleanlab has improved prediction accuracy\n", @@ -753,7 +575,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.10.8" } }, "nbformat": 4, diff --git a/docs/source/tutorials/token_classification.ipynb b/docs/source/tutorials/token_classification.ipynb index 9e261fd57e..938559540d 100644 --- a/docs/source/tutorials/token_classification.ipynb +++ b/docs/source/tutorials/token_classification.ipynb @@ -445,7 +445,7 @@ "id": "a35ef843", "metadata": {}, "source": [ - "### Find issue sentences with particular word \n", + "### Find sentences containing a particular mislabeled word \n", "\n", "You can also only focus on the subset of potentially problematic sentences where a particular token may have been mislabeled." ] @@ -470,7 +470,7 @@ "source": [ "### Sentence label quality score \n", "\n", - "For best reviewing label issues in a token classification dataset, you want to look at sentences one at a time. Here sentences more likely to contain a label error should be ranked earlier. Cleanlab can provide an overall label quality score for each sentence (ranging from 0 to 1) such that lower scores indicate sentences more likely to contain some mislabeled token. We can also obtain label quality scores for each individual token and decide which of these are label issues by thresholding them. This may be a superior approach if high precision (or high recall) is specifically preferred for your label error detection." + "For best reviewing label issues in a token classification dataset, you want to look at sentences one at a time. Here sentences more likely to contain a label error should be ranked earlier. Cleanlab can provide an overall label quality score for each sentence (ranging from 0 to 1) such that lower scores indicate sentences more likely to contain some mislabeled token. We can also obtain label quality scores for each individual token and manually decide which of these are label issues by thresholding them. For automatically estimating which tokens are mislabeled (and the number of label errors), you should use `find_label_issues()` instead. `get_label_quality_scores()` is useful if you only have time to review a few sentences and want to prioritize which, or if you're specifically aiming to detect label errors with high precision (or high recall) rather than overall estimation of the set of mislabeled tokens." ] }, { diff --git a/requirements-dev.txt b/requirements-dev.txt index 76a5b7898c..eb86e6968d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,14 +1,21 @@ # Python dependencies for development coverage != 6.3, != 6.3.* +datasets +hypothesis mypy pandas-stubs pre-commit +psutil pytest pytest-cov pytest-lazy-fixture requests scipy -torch -torchvision skorch tensorflow +torch +torchvision +typing-extensions>=4.1.1 +wget +matplotlib +pillow diff --git a/setup.cfg b/setup.cfg index 04a05501c6..82b8ba2d76 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,3 +5,9 @@ per-file-ignores = cleanlab/__init__.py: F401 cleanlab/token_classification/__init__.py: F401 cleanlab/benchmarking/__init__.py: F401 + cleanlab/regression/__init__.py: F401 + cleanlab/datalab/*/__init__.py: F401 + cleanlab/models/__init__.py: F401 + cleanlab/multilabel_classification/__init__.py: F401 + cleanlab/object_detection/__init__.py: F401 + cleanlab/segmentation/__init__.py: F401 diff --git a/setup.py b/setup.py index 71b685da50..d77953425d 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,17 @@ def run(self): # Get version number and store it in __version__ exec(open("cleanlab/version.py").read()) +DATALAB_REQUIRE = [ + # Mainly for Datalab's data storage class. + # Still some type hints that require datasets + "datasets>=2.7.0", +] + +EXTRAS_REQUIRE = { + "datalab": DATALAB_REQUIRE, + "all": ["matplotlib>=3.5.1"], +} +EXTRAS_REQUIRE["all"] = list(set(sum(EXTRAS_REQUIRE.values(), []))) setup( name="cleanlab", @@ -63,10 +74,8 @@ def run(self): "Programming Language :: Python", "Topic :: Software Development", "Topic :: Scientific/Engineering", - "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Mathematics", "Topic :: Scientific/Engineering :: Artificial Intelligence", - "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", ], @@ -89,10 +98,11 @@ def run(self): # requirements files see: # https://packaging.python.org/en/latest/discussions/install-requires-vs-requirements/ install_requires=[ - "numpy>=1.11.3", - "scikit-learn>=0.18", + "numpy>=1.20.0", + "scikit-learn>=1.0", "tqdm>=4.53.0", - "pandas>=1.0.0", - "termcolor>=1.1.0", + "pandas>=1.1.5", + "termcolor>=2.0.0", ], + extras_require=EXTRAS_REQUIRE, ) diff --git a/tests/datalab/conftest.py b/tests/datalab/conftest.py new file mode 100644 index 0000000000..e2d43197f7 --- /dev/null +++ b/tests/datalab/conftest.py @@ -0,0 +1,109 @@ +import numpy as np +import pandas as pd +import pytest +from datasets.arrow_dataset import Dataset +from sklearn.neighbors import NearestNeighbors + +from cleanlab.datalab.datalab import Datalab + +SEED = 42 +LABEL_NAME = "star" + + +@pytest.fixture +def dataset(): + data_dict = { + "id": [ + "7bd227d9-afc9-11e6-aba1-c4b301cdf627", + "7bd22905-afc9-11e6-a5dc-c4b301cdf627", + "7bd2299c-afc9-11e6-85d6-c4b301cdf627", + "7bd22a26-afc9-11e6-9309-c4b301cdf627", + "7bd22aba-afc9-11e6-8293-c4b301cdf627", + ], + "package_name": [ + "com.mantz_it.rfanalyzer", + "com.mantz_it.rfanalyzer", + "com.mantz_it.rfanalyzer", + "com.mantz_it.rfanalyzer", + "com.mantz_it.rfanalyzer", + ], + "review": [ + "Great app! The new version now works on my Bravia Android TV which is great as it's right by my rooftop aerial cable. The scan feature would be useful...any ETA on when this will be available? Also the option to import a list of bookmarks e.g. from a simple properties file would be useful.", + "Great It's not fully optimised and has some issues with crashing but still a nice app especially considering the price and it's open source.", + "Works on a Nexus 6p I'm still messing around with my hackrf but it works with my Nexus 6p Trond usb-c to usb host adapter. Thanks!", + "The bandwidth seemed to be limited to maximum 2 MHz or so. I tried to increase the bandwidth but not possible. I purchased this is because one of the pictures in the advertisement showed the 2.4GHz band with around 10MHz or more bandwidth. Is it not possible to increase the bandwidth? If not it is just the same performance as other free APPs.", + "Works well with my Hackrf Hopefully new updates will arrive for extra functions", + ], + "date": [ + "October 12 2016", + "August 23 2016", + "August 04 2016", + "July 25 2016", + "July 22 2016", + ], + "star": [4, 4, 5, 3, 5], + "version_id": [1487, 1487, 1487, 1487, 1487], + } + return Dataset.from_dict(data_dict) + + +@pytest.fixture +def label_name(): + return LABEL_NAME + + +@pytest.fixture +def lab(dataset, label_name): + return Datalab(data=dataset, label_name=label_name) + + +@pytest.fixture +def large_lab(): + np.random.seed(SEED) + N = 100 + K = 2 + data = np.random.rand(N, 2) + labels = np.random.randint(0, K, size=N) + pred_probs = np.random.rand(N, K) + pred_probs /= pred_probs.sum(axis=1, keepdims=True) + + lab = Datalab( + data={"features": data, "label": labels, "pred_probs": pred_probs}, label_name="label" + ) + knn = NearestNeighbors(n_neighbors=25, metric="euclidean").fit(data) + knn_graph = knn.kneighbors_graph(mode="distance") + lab.info["statistics"]["unit_test_knn_graph"] = knn_graph + return lab + + +@pytest.fixture +def pred_probs(dataset): + np.random.seed(SEED) + return np.random.rand(len(dataset), 3) + + +@pytest.fixture +def custom_issue_manager(): + from cleanlab.datalab.issue_manager.issue_manager import IssueManager + + class CustomIssueManager(IssueManager): + issue_name = "custom_issue" + + def find_issues(self, custom_argument: int = 1, **_) -> None: + # Flag example as an issue if the custom argument equals its index + scores = [ + abs(i - custom_argument) / (i + custom_argument) + for i in range(len(self.datalab.data)) + ] + self.issues = pd.DataFrame( + { + f"is_{self.issue_name}_issue": [ + i == custom_argument for i in range(len(self.datalab.data)) + ], + self.issue_score_key: scores, + }, + ) + summary_score = np.mean(scores) + self.summary = self.make_summary(score=summary_score) + + return CustomIssueManager diff --git a/tests/datalab/issue_manager/__init__.py b/tests/datalab/issue_manager/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/datalab/issue_manager/test_duplicate.py b/tests/datalab/issue_manager/test_duplicate.py new file mode 100644 index 0000000000..0be3038c47 --- /dev/null +++ b/tests/datalab/issue_manager/test_duplicate.py @@ -0,0 +1,82 @@ +import numpy as np +import pytest + +from cleanlab.datalab.issue_manager.duplicate import NearDuplicateIssueManager + +SEED = 42 + + +class TestNearDuplicateIssueManager: + @pytest.fixture + def embeddings(self, lab): + np.random.seed(SEED) + embeddings_array = 0.5 + 0.1 * np.random.rand(lab.get_info("statistics")["num_examples"], 2) + embeddings_array[4, :] = ( + embeddings_array[3, :] + np.random.rand(embeddings_array.shape[1]) * 0.001 + ) + return {"embedding": embeddings_array} + + @pytest.fixture + def issue_manager(self, lab, embeddings, monkeypatch): + mock_data = lab.data.from_dict({**lab.data.to_dict(), **embeddings}) + monkeypatch.setattr(lab, "data", mock_data) + return NearDuplicateIssueManager( + datalab=lab, + metric="euclidean", + k=2, + ) + + def test_init(self, lab, issue_manager): + assert issue_manager.datalab == lab + assert issue_manager.metric == "euclidean" + assert issue_manager.k == 2 + assert issue_manager.threshold == 0.13 + + issue_manager = NearDuplicateIssueManager( + datalab=lab, + threshold=0.1, + ) + assert issue_manager.threshold == 0.1 + + def test_find_issues(self, issue_manager, embeddings): + issue_manager.find_issues(features=embeddings["embedding"]) + issues, summary, info = issue_manager.issues, issue_manager.summary, issue_manager.info + expected_issue_mask = np.array([False] * 3 + [True] * 2) + assert np.all( + issues["is_near_duplicate_issue"] == expected_issue_mask + ), "Issue mask should be correct" + assert summary["issue_type"][0] == "near_duplicate" + assert summary["score"][0] == pytest.approx(expected=0.03122489, abs=1e-7) + + assert ( + info.get("near_duplicate_sets", None) is not None + ), "Should have sets of near duplicates" + + new_issue_manager = NearDuplicateIssueManager( + datalab=issue_manager.datalab, + metric="euclidean", + k=2, + threshold=0.1, + ) + new_issue_manager.find_issues(features=embeddings["embedding"]) + + def test_report(self, issue_manager, embeddings): + issue_manager.find_issues(features=embeddings["embedding"]) + report = issue_manager.report( + issues=issue_manager.issues, + summary=issue_manager.summary, + info=issue_manager.info, + ) + assert isinstance(report, str) + assert ( + "------------------ near_duplicate issues -------------------\n\n" + "Number of examples with this issue:" + ) in report + + report = issue_manager.report( + issues=issue_manager.issues, + summary=issue_manager.summary, + info=issue_manager.info, + verbosity=3, + ) + assert "Additional Information: " in report diff --git a/tests/datalab/issue_manager/test_label.py b/tests/datalab/issue_manager/test_label.py new file mode 100644 index 0000000000..35ee4b8b93 --- /dev/null +++ b/tests/datalab/issue_manager/test_label.py @@ -0,0 +1,105 @@ +import numpy as np +import pandas as pd +import pytest + +from cleanlab.datalab.issue_manager.label import LabelIssueManager + + +class TestLabelIssueManager: + @pytest.fixture + def issue_manager(self, lab): + return LabelIssueManager(datalab=lab) + + def test_find_issues(self, pred_probs, issue_manager): + """Test that the find_issues method works.""" + issue_manager.find_issues(pred_probs=pred_probs) + issues, summary, info = issue_manager.issues, issue_manager.summary, issue_manager.info + assert isinstance(issues, pd.DataFrame), "Issues should be a dataframe" + + assert isinstance(summary, pd.DataFrame), "Summary should be a dataframe" + assert summary["issue_type"].values[0] == "label" + assert pytest.approx(summary["score"].values[0]) == 0.6 + + assert isinstance(info, dict), "Info should be a dict" + info_keys = info.keys() + expected_keys = [ + "num_label_issues", + "average_label_quality", + "confident_joint", + "classes_by_label_quality", + "overlapping_classes", + "py", + "noise_matrix", + "inverse_noise_matrix", + ] + assert all( + [key in info_keys for key in expected_keys] + ), f"Info should have the right keys, but is missing {set(expected_keys) - set(info_keys)}" + + def test_find_issues_with_kwargs(self, pred_probs, issue_manager): + issue_manager.find_issues(pred_probs=pred_probs, thresholds=[0.2, 0.3, 0.1]) + + def test_init_with_clean_learning_kwargs(self, lab, issue_manager): + """Test that the init method can provide kwargs to the CleanLearning constructor.""" + new_issue_manager = LabelIssueManager( + datalab=lab, + clean_learning_kwargs={"cv_n_folds": 10}, + ) + cv_n_folds = [im.cl.cv_n_folds for im in [issue_manager, new_issue_manager]] + assert cv_n_folds == [5, 10], "Issue manager should have the right attributes" + + def test_get_summary_parameters(self, issue_manager, monkeypatch): + mock_health_summary_parameters = { + "labels": [1, 0, 2], + "asymmetric": False, + "class_names": ["a", "b", "c"], + "num_examples": 3, + "joint": [1 / 3, 1 / 3, 1 / 3], + "confident_joint": [1 / 3, 1 / 3, 1 / 3], + "multi_label": False, + } + pred_probs = np.random.rand(3, 3) + monkeypatch.setattr( + issue_manager, "health_summary_parameters", mock_health_summary_parameters + ) + summary_parameters = issue_manager._get_summary_parameters(pred_probs=pred_probs) + expected_parameters = { + "confident_joint": [1 / 3, 1 / 3, 1 / 3], + "asymmetric": False, + "class_names": ["a", "b", "c"], + } + assert summary_parameters == expected_parameters + + # Test missing "confident_joint" key + mock_health_summary_parameters.pop("confident_joint") + monkeypatch.setattr( + issue_manager, "health_summary_parameters", mock_health_summary_parameters + ) + summary_parameters = issue_manager._get_summary_parameters(pred_probs=pred_probs) + expected_parameters = { + "joint": [1 / 3, 1 / 3, 1 / 3], + "num_examples": 3, + "asymmetric": False, + "class_names": ["a", "b", "c"], + } + assert summary_parameters == expected_parameters + + # Test missing "joint" key + mock_health_summary_parameters.pop("joint") + monkeypatch.setattr( + issue_manager.datalab._labels, "labels", mock_health_summary_parameters["labels"] + ) + monkeypatch.setattr( + issue_manager, "health_summary_parameters", mock_health_summary_parameters + ) + summary_parameters = issue_manager._get_summary_parameters(pred_probs=pred_probs) + expected_parameters = { + "pred_probs": pred_probs, + "labels": [1, 0, 2], + "asymmetric": False, + "class_names": ["a", "b", "c"], + } + assert np.all(summary_parameters["pred_probs"] == expected_parameters["pred_probs"]) + summary_parameters.pop("pred_probs") + expected_parameters.pop("pred_probs") + assert summary_parameters == expected_parameters diff --git a/tests/datalab/issue_manager/test_noniid.py b/tests/datalab/issue_manager/test_noniid.py new file mode 100644 index 0000000000..0569b41e69 --- /dev/null +++ b/tests/datalab/issue_manager/test_noniid.py @@ -0,0 +1,223 @@ +import numpy as np +import pytest + +from cleanlab.datalab.issue_manager.noniid import ( + NonIIDIssueManager, + simplified_kolmogorov_smirnov_test, +) + +SEED = 42 + + +@pytest.mark.parametrize( + "neighbor_histogram, non_neighbor_histogram, expected_statistic", + [ + # Test with equal histograms + ( + [0.25, 0.25, 0.25, 0.25], + [0.25, 0.25, 0.25, 0.25], + 0.0, + ), + # Test with maximum difference in the first bin + ( + [1.0, 0.0, 0.0, 0.0], + [0.0, 0.25, 0.25, 0.5], + 1.0, + ), + # Test with maximum difference in the last bin + ( + [0.25, 0.25, 0.25, 0.25], + [0.5, 0.25, 0.25, 0.0], + 0.25, + ), + # Test with arbitrary histograms + ( + [0.2, 0.3, 0.4, 0.1], + [0.1, 0.4, 0.25, 0.3], + 0.15, # (0.2 -> 0.5 -> *0.9* -> 1.0) vs (0.1 -> 0.5 -> *0.75* -> 1.05 + ), + ], + ids=[ + "equal_histograms", + "maximum_difference_in_first_bin", + "maximum_difference_in_last_bin", + "arbitrary_histograms", + ], +) +def test_simplified_kolmogorov_smirnov_test( + neighbor_histogram, non_neighbor_histogram, expected_statistic +): + nh = np.array(neighbor_histogram) + nnh = np.array(non_neighbor_histogram) + statistic = simplified_kolmogorov_smirnov_test(nh, nnh) + np.testing.assert_almost_equal(statistic, expected_statistic) + + +class TestNonIIDIssueManager: + @pytest.fixture + def embeddings(self, lab): + np.random.seed(SEED) + embeddings_array = np.arange(lab.get_info("statistics")["num_examples"] * 10).reshape(-1, 1) + return embeddings_array + + @pytest.fixture + def issue_manager(self, lab): + return NonIIDIssueManager( + datalab=lab, + metric="euclidean", + k=10, + ) + + def test_init(self, lab, issue_manager): + assert issue_manager.datalab == lab + assert issue_manager.metric == "euclidean" + assert issue_manager.k == 10 + assert issue_manager.num_permutations == 25 + assert issue_manager.significance_threshold == 0.05 + + issue_manager = NonIIDIssueManager( + datalab=lab, + num_permutations=15, + ) + + assert issue_manager.num_permutations == 15 + + def test_find_issues(self, issue_manager, embeddings): + np.random.seed(SEED) + issue_manager.find_issues(features=embeddings) + issues_sort, summary_sort, info_sort = ( + issue_manager.issues, + issue_manager.summary, + issue_manager.info, + ) + expected_sorted_issue_mask = np.array([False] * 46 + [True] + [False] * 3) + assert np.all( + issues_sort["is_non_iid_issue"] == expected_sorted_issue_mask + ), "Issue mask should be correct" + assert summary_sort["issue_type"][0] == "non_iid" + assert summary_sort["score"][0] == pytest.approx(expected=0.0, abs=1e-7) + assert info_sort.get("p-value", None) is not None, "Should have p-value" + assert summary_sort["score"][0] == pytest.approx(expected=info_sort["p-value"], abs=1e-7) + + permutation = np.random.permutation(len(embeddings)) + new_issue_manager = NonIIDIssueManager( + datalab=issue_manager.datalab, + metric="euclidean", + k=10, + ) + new_issue_manager.find_issues(features=embeddings[permutation]) + issues_perm, summary_perm, info_perm = ( + new_issue_manager.issues, + new_issue_manager.summary, + new_issue_manager.info, + ) + expected_permuted_issue_mask = np.array([False] * len(embeddings)) + assert np.all( + issues_perm["is_non_iid_issue"] == expected_permuted_issue_mask + ), "Issue mask should be correct" + assert summary_perm["issue_type"][0] == "non_iid" + # ensure score is large, cannot easily ensure precise value because random seed has different effects on different OS: + assert summary_perm["score"][0] > 0.05 + assert info_perm.get("p-value", None) is not None, "Should have p-value" + assert summary_perm["score"][0] == pytest.approx(expected=info_perm["p-value"], abs=1e-7) + + def test_report(self, issue_manager, embeddings): + np.random.seed(SEED) + issue_manager.find_issues(features=embeddings) + report = issue_manager.report( + issues=issue_manager.issues, + summary=issue_manager.summary, + info=issue_manager.info, + ) + + assert isinstance(report, str) + assert ( + "---------------------- non_iid issues ----------------------\n\n" + "Number of examples with this issue:" + ) in report + + report = issue_manager.report( + issues=issue_manager.issues, + summary=issue_manager.summary, + info=issue_manager.info, + verbosity=3, + ) + assert "Additional Information: " in report + + def test_collect_info(self, issue_manager, embeddings): + """Test some values in the info dict. + + Mainly focused on the nearest neighbor info. + """ + + issue_manager.find_issues(features=embeddings) + info = issue_manager.info + + assert info["p-value"] == 0 + assert info["metric"] == "euclidean" + assert info["k"] == 10 + + @pytest.mark.parametrize( + "seed", + [ + "default", + SEED, + None, + ], + ids=["default", "seed", "no_seed"], + ) + def test_seed(self, lab, seed): + num_classes = 10 + means = [ + np.array([np.random.uniform(high=10), np.random.uniform(high=10)]) + for _ in range(num_classes) + ] + sigmas = [np.random.uniform(high=1) for _ in range(num_classes)] + class_stats = list(zip(means, sigmas)) + num_samples = 2000 + + def generate_data_iid(): + # This should be IID, resulting in a larger p-value + samples = [] + labels = [] + + for _ in range(num_samples): + label = np.random.choice(num_classes) + mean, sigma = class_stats[label] + sample = np.random.normal(mean, sigma) + samples.append(sample) + labels.append(label) + samples = np.array(samples) + labels = np.array(labels) + dataset = {"features": samples, "labels": labels} + return dataset + + dataset = generate_data_iid() + embeddings = dataset["features"] + + # Create new issue manager, ignore the lab assigned for this test + if seed == "default": + issue_manager = NonIIDIssueManager( + datalab=lab, + metric="euclidean", + k=10, + ) + else: + issue_manager = NonIIDIssueManager( + datalab=lab, + metric="euclidean", + k=10, + seed=seed, + ) + issue_manager.find_issues(features=embeddings) + p_value = issue_manager.info["p-value"] + + # Run again with the same seed + issue_manager.find_issues(features=embeddings) + p_value2 = issue_manager.info["p-value"] + + assert p_value > 0.0 + if seed is not None or seed == "default": + assert p_value == p_value2 + else: + assert p_value != p_value2 diff --git a/tests/datalab/issue_manager/test_outlier.py b/tests/datalab/issue_manager/test_outlier.py new file mode 100644 index 0000000000..4c05bbf916 --- /dev/null +++ b/tests/datalab/issue_manager/test_outlier.py @@ -0,0 +1,178 @@ +import numpy as np +import pandas as pd +import pytest + +from cleanlab.datalab.issue_manager.outlier import OutlierIssueManager +from cleanlab.outlier import OutOfDistribution + +SEED = 42 + + +class TestOutlierIssueManager: + @pytest.fixture + def embeddings(self, lab): + np.random.seed(SEED) + embeddings_array = 0.5 + 0.1 * np.random.rand(lab.get_info("statistics")["num_examples"], 2) + embeddings_array[4, :] = -1 + return {"embedding": embeddings_array} + + @pytest.fixture + def issue_manager(self, lab): + return OutlierIssueManager(datalab=lab, k=3) + + @pytest.fixture + def issue_manager_with_threshold(self, lab): + return OutlierIssueManager(datalab=lab, k=2, threshold=0.5) + + def test_init(self, issue_manager, issue_manager_with_threshold): + assert isinstance(issue_manager.ood, OutOfDistribution) + assert issue_manager.ood.params["k"] == 3 + assert issue_manager.threshold == None + + assert issue_manager_with_threshold.ood.params["k"] == 2 + assert issue_manager_with_threshold.threshold == 0.5 + + def test_find_issues(self, issue_manager, issue_manager_with_threshold, embeddings): + issue_manager.find_issues(features=embeddings["embedding"]) + issues, summary, info = issue_manager.issues, issue_manager.summary, issue_manager.info + expected_issue_mask = np.array([False] * 4 + [True]) + assert np.all( + issues["is_outlier_issue"] == expected_issue_mask + ), "Issue mask should be correct" + assert summary["issue_type"][0] == "outlier" + assert summary["score"][0] == pytest.approx(expected=0.7732146, abs=1e-7) + + assert info.get("knn", None) is not None, "Should have knn info" + assert issue_manager.threshold == pytest.approx(expected=0.37037, abs=1e-5) + + issue_manager_with_threshold.find_issues(features=embeddings["embedding"]) + + def test_find_issues_with_pred_probs(self, lab): + issue_manager = OutlierIssueManager(datalab=lab, threshold=0.3) + pred_probs = np.array( + [ + [0.25, 0.725, 0.025], + [0.37, 0.42, 0.21], + [0.05, 0.05, 0.9], + [0.1, 0.05, 0.85], + [0.1125, 0.65, 0.2375], + ] + ) + issue_manager.find_issues(pred_probs=pred_probs) + issues, summary, info = issue_manager.issues, issue_manager.summary, issue_manager.info + expected_issue_mask = np.array([False] * 4 + [True]) + assert np.all( + issues["is_outlier_issue"] == expected_issue_mask + ), "Issue mask should be correct" + assert summary["issue_type"][0] == "outlier" + assert summary["score"][0] == pytest.approx(expected=0.210, abs=1e-3) + + assert issue_manager.threshold == 0.3 + + assert np.all( + info.get("confident_thresholds", None) == [0.1, 0.5725, 0.56875] + ), "Should have confident_joint info" + + def test_find_issues_with_different_thresholds(self, lab, embeddings): + issue_manager = OutlierIssueManager(datalab=lab, k=3, threshold=0.66666) + issue_manager.find_issues(features=embeddings["embedding"]) + issues, summary, info = issue_manager.issues, issue_manager.summary, issue_manager.info + expected_issue_mask = np.array([False] * 4 + [True]) + assert np.all( + issues["is_outlier_issue"] == expected_issue_mask + ), "Issue mask should be correct" + assert summary["issue_type"][0] == "outlier" + assert summary["score"][0] == pytest.approx(expected=0.7732146, abs=1e-7) + + assert issue_manager.threshold == 0.66666 + + def test_report(self, issue_manager): + pred_probs = np.array( + [ + [0.1, 0.85, 0.05], + [0.15, 0.8, 0.05], + [0.05, 0.05, 0.9], + [0.1, 0.05, 0.85], + [0.1, 0.65, 0.25], + ] + ) + issue_manager.find_issues(pred_probs=pred_probs) + report = issue_manager.report( + issues=issue_manager.issues, + summary=issue_manager.summary, + info=issue_manager.info, + ) + assert isinstance(report, str) + assert ( + "---------------------- outlier issues ----------------------\n\n" + "Number of examples with this issue:" + ) in report + + report = issue_manager.report( + issues=issue_manager.issues, + summary=issue_manager.summary, + info=issue_manager.info, + verbosity=3, + ) + assert "Additional Information: " in report + + # Mock some vector and matrix values in the info dict + mock_info = issue_manager.info + vector = np.array([1, 2, 3, 4, 5, 6]) + matrix = np.array([[i for i in range(20)] for _ in range(10)]) + df = pd.DataFrame(matrix) + mock_list = [9, 8, 7, 6, 5, 4, 3, 2, 1] + mock_dict = {"a": 1, "b": 2, "c": 3} + mock_info["vector"] = vector + mock_info["matrix"] = matrix + mock_info["list"] = mock_list + mock_info["dict"] = mock_dict + mock_info["df"] = df + + report = issue_manager.report( + issues=issue_manager.issues, + summary=issue_manager.summary, + info={**issue_manager.info, **mock_info}, + verbosity=4, + ) + assert "Additional Information: " in report + assert "vector: [1, 2, 3, 4, '...']" in report + assert f"matrix: array of shape {matrix.shape}\n[[ 0 " in report + assert "list: [9, 8, 7, 6, '...']" in report + assert 'dict:\n{\n "a": 1,\n "b": 2,\n "c": 3\n}' in report + assert "df:" in report + + report = issue_manager.report( + issues=issue_manager.issues, + summary=issue_manager.summary, + info={**issue_manager.info, **mock_info}, + verbosity=2, + ) + assert "Additional Information: " in report + assert "vector: [1, 2, 3, 4, '...']" not in report + assert f"matrix: array of shape {matrix.shape}\n[[ 0 " not in report + assert "list: [9, 8, 7, 6, '...']" not in report + assert 'dict:\n{\n "a": 1,\n "b": 2,\n "c": 3\n}' not in report + assert "df:" not in report + + def test_collect_info(self, issue_manager, embeddings): + """Test some values in the info dict. + + Mainly focused on the nearest neighbor info. + """ + + issue_manager.find_issues(features=embeddings["embedding"]) + info = issue_manager.info + + nearest_neighbors = info["nearest_neighbor"] + distances_to_nearest_neighbor = info["distance_to_nearest_neighbor"] + + assert nearest_neighbors == [3, 0, 3, 0, 2], "Nearest neighbors should be correct" + + assert pytest.approx(distances_to_nearest_neighbor, abs=1e-3) == [ + 0.033, + 0.05, + 0.072, + 0.033, + 2.143, + ], "Distances to nearest neighbor should be correct" diff --git a/tests/datalab/test_data.py b/tests/datalab/test_data.py new file mode 100644 index 0000000000..48677d79d5 --- /dev/null +++ b/tests/datalab/test_data.py @@ -0,0 +1,163 @@ +import tempfile +from unittest.mock import patch +import pytest +from cleanlab.datalab.data import Data, DataFormatError, DatasetLoadError +from datasets import Dataset, ClassLabel +import numpy as np +import hypothesis.strategies as st +from hypothesis import given, assume, settings, HealthCheck + + +NUM_COLS = 2 + + +@st.composite +def dataset_strategy(draw): + # Deifine strategies + int_feature_strategy = st.integers(min_value=-10, max_value=10) + float_feature_strategy = st.floats(min_value=-10, max_value=10) + column_name_strategy = st.text(min_size=5, max_size=5) + column_data_strategy = st.one_of(int_feature_strategy, float_feature_strategy) + + # Draw values + col_names = draw( + st.lists(column_name_strategy, min_size=NUM_COLS, max_size=NUM_COLS + 1, unique=True) + ) + label_name = draw(st.sampled_from(col_names)) + data = { + name: draw(st.lists(column_data_strategy, min_size=5, max_size=5)) for name in col_names + } + dataset = Dataset.from_dict(data) + dataset = dataset.rename_column(label_name, "label") + + # Make assertions about drawn values + assume(len(set(dataset["label"])) > 1) + + return dataset + + +class TestData: + @pytest.fixture + def dataset_and_label_name(self): + label_name = "labels" + + dataset = Dataset.from_dict({"image": [1, 2, 3], label_name: [0, 1, 0]}) + return dataset, label_name + + @pytest.mark.slow + @given(dataset=dataset_strategy()) + @settings(max_examples=10, suppress_health_check=[HealthCheck.too_slow]) + def test_init_data_properties(self, dataset): + data = Data(data=dataset, label_name="label") + assert data._data == dataset + + # All elements in the _labels attribute are integers in the range [0, num_classes - 1] + num_classes = len(set(data.labels.label_map)) + all_labels_are_ints = np.issubdtype(data.labels.labels.dtype, np.integer) + assert all_labels_are_ints, f"{data.labels.labels} should be a list of integers" + assert all(0 <= label < num_classes for label in data.labels.labels) + + assert all(isinstance(label, int) for label in data.labels.label_map.keys()) + + def test_init_data(self, dataset_and_label_name): + dataset, label_name = dataset_and_label_name + data = Data(data=dataset, label_name=label_name) + + label_feature = dataset.features[label_name] + if isinstance(label_feature, ClassLabel): + classes = label_feature.names + else: + classes = sorted(dataset.unique(label_name)) + assert data.class_names == classes + + def test_init_data_from_list_of_dicts(self): + dataset = [{"X": 0, "label": 0}, {"X": 1, "label": 1}, {"X": 2, "label": 1}] + data = Data(data=dataset, label_name="label") + assert isinstance(data._data, Dataset) + + def test_init_raises_format_error(self): + data = np.random.rand(10, 2) + with pytest.raises(DataFormatError) as excinfo: + Data(data=data, label_name="label") + + expected_error_substring = "Unsupported data type: \n" + assert expected_error_substring in str(excinfo.value) + + def test_init_raises_load_error(self): + improperly_aligned_data = { + "X": [0, 1, 2], + "label": [0, 1], + } + with pytest.raises(DatasetLoadError) as excinfo: + Data(data=improperly_aligned_data, label_name="label") + + expected_error_substring = "Failed to load dataset from .\n" + assert expected_error_substring in str(excinfo.value) + + def test_not_equal_to_copy_or_non_data(self): + dataset = {"X": [0, 1, 2], "label": [0, 1, 2]} + data = Data(data=dataset, label_name="label") + data_copy = Data(data=dataset, label_name="label") + assert data != data_copy + assert data != dataset + + def test_load_dataset_from_string(self, monkeypatch): + # Test with non-existent file + with pytest.raises(DatasetLoadError): + Data._load_dataset_from_string("non_existent_file.txt") + + # Test with invalid extension + with tempfile.NamedTemporaryFile(suffix=".invalid") as temp_file: + with pytest.raises(DatasetLoadError): + Data._load_dataset_from_string(temp_file.name) + + # Test with invalid external dataset identifier + with patch("datasets.load_dataset") as mock_load_dataset: + mock_load_dataset.side_effect = ValueError("Invalid external dataset identifier") + with pytest.raises(DatasetLoadError) as excinfo: + Data._load_dataset_from_string("invalid_external_dataset_name") + + expected_error_substring = "Failed to load dataset from .\n" + assert expected_error_substring in str(excinfo.value) + + # Test with valid .txt, .csv, and .json files + test_data = [ + (".txt", "sample text", "from_text"), + (".csv", "column1,column2\nvalue1,value2", "from_csv"), + (".json", '{"key": "value"}', "from_json"), + ] + + mock_dataset = Dataset.from_dict({"y": [1, 2, 3]}) + for ext, content, loader_func in test_data: + with tempfile.NamedTemporaryFile(suffix=ext, mode="w+t") as temp_file: + temp_file.write(content) + temp_file.flush() + + # Make sure the correct loader function is called + def fake_loader(file_name): + assert file_name == temp_file.name + return mock_dataset + + with monkeypatch.context() as mp: + mp.setattr(Dataset, loader_func, fake_loader) + loaded_dataset = Data._load_dataset_from_string(temp_file.name) + assert isinstance(loaded_dataset, Dataset) + assert loaded_dataset == mock_dataset + + # Test with an external dataset + def fake_load_dataset(data_string): + if data_string == "external_dataset": + return mock_dataset + + raise Exception("Not the expected dataset string") + + with monkeypatch.context() as mp: + mp.setattr("datasets.load_dataset", fake_load_dataset) + loaded_dataset = Data._load_dataset_from_string("external_dataset") + assert isinstance(loaded_dataset, Dataset) + assert loaded_dataset == mock_dataset + + with pytest.raises(DatasetLoadError) as excinfo: + Data._load_dataset_from_string("non_external_dataset") + + expected_error_substring = "Failed to load dataset from .\n" diff --git a/tests/datalab/test_data_issues.py b/tests/datalab/test_data_issues.py new file mode 100644 index 0000000000..272200067c --- /dev/null +++ b/tests/datalab/test_data_issues.py @@ -0,0 +1,46 @@ +import pytest +from cleanlab.datalab.data import Data +from cleanlab.datalab.data_issues import DataIssues + + +class TestDataIssues: + labels = ["B", "A", "B"] + label_name = "labels" + + @pytest.fixture + def data_issues(self): + data = Data(data={self.label_name: self.labels}, label_name=self.label_name) + data_issues = DataIssues(data=data) + yield data_issues + + def test_data_issues_init(self, data_issues): + assert hasattr(data_issues, "issues") + assert hasattr(data_issues, "issue_summary") + assert hasattr(data_issues, "info") + + def test_statistics(self, data_issues): + stats = data_issues.statistics + + assert stats == data_issues.info["statistics"] + assert stats["num_examples"] == 3, f"Incorrect number of examples: {stats['num_examples']}" + assert stats["class_names"] == ["A", "B"], f"Incorrect class names: {stats['class_names']}" + assert stats["num_classes"] == 2, f"Incorrect number of classes: {stats['num_classes']}" + assert stats["multi_label"] is False + assert ( + stats["health_score"] is None + ), f"Health score should initially be None, but is {stats['health_score']}" + + def test_get_info(self, data_issues): + with pytest.raises(ValueError): + data_issues.get_info("nonexistent_issue") + assert data_issues.get_info("statistics") == data_issues.info["statistics"] + + def test_get_info_label(self, data_issues): + data_issues.info["label"] = {"given_label": [0, 1, 1], "predicted_label": [1, 0, 1]} + info = data_issues.get_info("label") + + label_format_error_message = ( + "get_info('label') should return the given label formatted with the class names" + ) + assert info.get("given_label").tolist() == ["A", "B", "B"], label_format_error_message + assert info.get("predicted_label").tolist() == self.labels, label_format_error_message diff --git a/tests/datalab/test_datalab.py b/tests/datalab/test_datalab.py new file mode 100644 index 0000000000..9932a2f1f8 --- /dev/null +++ b/tests/datalab/test_datalab.py @@ -0,0 +1,934 @@ +# Copyright (C) 2017-2022 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . + + +import contextlib +import io +import os +import pickle +from unittest.mock import MagicMock, Mock, patch +from cleanlab.datalab.datalab import Datalab + +from scipy.sparse import csr_matrix +from sklearn.neighbors import NearestNeighbors +from datasets.dataset_dict import DatasetDict +import numpy as np +import pandas as pd + +from pathlib import Path + +import pytest +import timeit + +from cleanlab.datalab.report import Reporter + +SEED = 42 + + +def test_datalab_invalid_datasetdict(dataset, label_name): + with pytest.raises(ValueError) as e: + datadict = DatasetDict({"train": dataset, "test": dataset}) + Datalab(datadict, label_name) # type: ignore + assert "Please pass a single dataset, not a DatasetDict." in str(e) + + +class TestDatalab: + """Tests for the Datalab class.""" + + @pytest.fixture + def lab(self, dataset, label_name): + return Datalab(data=dataset, label_name=label_name) + + def test_print(self, lab, capsys): + # Can print the object + print(lab) + captured = capsys.readouterr() + expected_output = ( + "Datalab:\n" + "Checks run: No\n" + "Number of examples: 5\n" + "Number of classes: 3\n" + "Issues identified: Not checked\n" + ) + assert expected_output == captured.out + + def test_class_names(self): + y = ["a", "3", "2", "3"] + lab = Datalab({"y": y}, label_name="y") + assert lab.class_names == ["2", "3", "a"] + + y = [-1, 4, 0.5, 0, 4, -1] + lab = Datalab({"y": y}, label_name="y") + assert lab.class_names == [-1, 0, 0.5, 4] + + def test_list_default_issue_types(self): + assert Datalab.list_default_issue_types() == [ + "label", + "outlier", + "near_duplicate", + "non_iid", + ] + + def tmp_path(self): + # A path for temporarily saving the instance during tests. + # This is a workaround for the fact that the Datalab class + # does not have a save method. + return Path(__file__).parent / "tmp.pkl" + + def test_attributes(self, lab): + # Has the right attributes + for attr in ["data", "label_name", "_labels", "info", "issues"]: + assert hasattr(lab, attr), f"Missing attribute {attr}" + + assert all(lab.labels == np.array([1, 1, 2, 0, 2])) + assert isinstance(lab.issues, pd.DataFrame), "Issues should by in a dataframe" + assert isinstance(lab.issue_summary, pd.DataFrame), "Issue summary should be a dataframe" + + def test_get_info(self, lab): + mock_info: dict = { + "label": { + "given_label": [1, 0, 1, 0, 2], + "predicted_label": [1, 1, 2, 0, 2], + # get_info("label") adds `class_names` from statistics + }, + "near_duplicate": { + "nearest_neighbor": [1, 0, 0, 4, 3], + }, + } + mock_info = {**lab.info, **mock_info} + lab.info = mock_info + + label_info = lab.get_info("label") + assert label_info["given_label"].tolist() == [4, 3, 4, 3, 5] + assert label_info["predicted_label"].tolist() == [4, 4, 5, 3, 5] + assert label_info["class_names"] == [3, 4, 5] + + near_duplicate_info = lab.get_info("near_duplicate") + assert near_duplicate_info["nearest_neighbor"] == [1, 0, 0, 4, 3] + + assert lab.get_info() == lab.info == mock_info + + def test_get_issue_summary(self, lab, monkeypatch): + mock_summary: pd.DataFrame = pd.DataFrame( + { + "issue_type": ["label", "outlier"], + "score": [0.5, 0.3], + "num_issues": [1, 2], + } + ) + monkeypatch.setattr(lab, "issue_summary", mock_summary) + + label_summary = lab.get_issue_summary(issue_name="label") + pd.testing.assert_frame_equal(label_summary, mock_summary.iloc[[0]]) + + outlier_summary = lab.get_issue_summary(issue_name="outlier") + pd.testing.assert_frame_equal( + outlier_summary, mock_summary.iloc[[1]].reset_index(drop=True) + ) + + summary = lab.get_issue_summary() + pd.testing.assert_frame_equal(summary, mock_summary) + + def test_get_issues(self, lab, monkeypatch): + mock_issues: pd.DataFrame = pd.DataFrame( + { + "is_label_issue": [True, False, False, True, False], + "label_score": [0.2, 0.4, 0.6, 0.1, 0.8], + "is_near_duplicate_issue": [False, True, True, False, True], + "near_duplicate_score": [0.5, 0.3, 0.1, 0.7, 0.2], + }, + ) + monkeypatch.setattr(lab, "issues", mock_issues) + + mock_predicted_labels = np.array([0, 1, 2, 1, 2]) + + mock_distance_to_nearest_neighbor = [0.1, 0.2, 0.3, 0.4, 0.5] + + lab.info.update( + { + "label": { + "given_label": lab.labels, + "predicted_label": mock_predicted_labels, + }, + "near_duplicate": { + "distance_to_nearest_neighbor": mock_distance_to_nearest_neighbor, + }, + } + ) + + label_issues = lab.get_issues(issue_name="label") + + expected_label_issues = pd.DataFrame( + { + **{key: mock_issues[key] for key in ["is_label_issue", "label_score"]}, + "given_label": [4, 4, 5, 3, 5], + "predicted_label": [3, 4, 5, 4, 5], + }, + ) + + pd.testing.assert_frame_equal(label_issues, expected_label_issues, check_dtype=False) + + near_duplicate_issues = lab.get_issues(issue_name="near_duplicate") + + expected_near_duplicate_issues = pd.DataFrame( + { + **{ + key: mock_issues[key] + for key in ["is_near_duplicate_issue", "near_duplicate_score"] + }, + "distance_to_nearest_neighbor": mock_distance_to_nearest_neighbor, + }, + ) + pd.testing.assert_frame_equal( + near_duplicate_issues, expected_near_duplicate_issues, check_dtype=False + ) + + issues = lab.get_issues() + pd.testing.assert_frame_equal(issues, mock_issues, check_dtype=False) + + @pytest.mark.parametrize( + "issue_types", + [None, {"label": {}}], + ids=["Default issues", "Only label issues"], + ) + def test_find_issues_with_pred_probs(self, lab, pred_probs, issue_types): + assert lab.issues.empty, "Issues should be empty before calling find_issues" + assert lab.issue_summary.empty, "Issue summary should be empty before calling find_issues" + assert lab.info["statistics"]["health_score"] is None + lab.find_issues(pred_probs=pred_probs, issue_types=issue_types) + assert not lab.issues.empty, "Issues weren't updated" + assert not lab.issue_summary.empty, "Issue summary wasn't updated" + assert ( + lab.info["statistics"]["health_score"] == lab.issue_summary["score"].mean() + ) # TODO: Avoid re-implementing logic in test + + if issue_types is None: + # Test default issue types + columns = lab.issues.columns + for issue_type in ["label", "outlier"]: + assert f"is_{issue_type}_issue" in columns + assert f"{issue_type}_score" in columns + + def test_find_issues_without_values_in_issue_types_raises_warning(self, lab, pred_probs): + issue_types = {} + with pytest.warns(UserWarning) as record: + lab.find_issues(pred_probs=pred_probs, issue_types=issue_types) + warning_message = record[0].message.args[0] + assert ( + "No issue types were specified. No issues will be found in the dataset." + in warning_message + ) + + @pytest.mark.parametrize( + "issue_types", + [ + None, + {"label": {}}, + {"outlier": {}}, + {"near_duplicate": {}}, + {"non_iid": {}}, + {"outlier": {}, "near_duplicate": {}}, + ], + ids=[ + "Defaults", + "Only label issues", + "Only outlier issues", + "Only near_duplicate issues", + "Only non_iid issues", + "Both outlier and near_duplicate issues", + ], + ) + @pytest.mark.parametrize( + "use_features", + [True, False], + ids=["Use features", "Don't use features"], + ) + @pytest.mark.parametrize( + "use_pred_probs", + [True, False], + ids=["Use pred_probs", "Don't use pred_probs"], + ) + @pytest.mark.parametrize( + "use_knn_graph", + [True, False], + ids=["Use knn_graph", "Don't use knn_graph"], + ) + def test_repeat_find_issues_then_report_with_defaults( + self, + large_lab, + issue_types, + use_features, + use_pred_probs, + use_knn_graph, + ): + """Test "all" combinations of inputs to find_issues() and make sure repeated calls to it won't change any results. Same applies to report(). + + This test does NOT test the correctness of the inputs, so some test cases may lead to missing arguments errors that are silently ignored. + """ + + # Extract features and pred_probs from Datalab object + features, pred_probs = ( + np.array(large_lab.data[k]) if v else None + for k, v in zip(["features", "pred_probs"], [use_features, use_pred_probs]) + ) + + # Extract sparse knn_graph from Datalab object's info dictionary + knn_graph = None + if use_knn_graph: + knn_graph = large_lab.info["statistics"]["unit_test_knn_graph"] + + # Run find_issues and report() once + large_lab.find_issues( + features=features, pred_probs=pred_probs, knn_graph=knn_graph, issue_types=issue_types + ) + with contextlib.redirect_stdout(io.StringIO()) as f: + large_lab.report() + first_report = f.getvalue() + issues = large_lab.issues.copy() + issue_summary = large_lab.issue_summary.copy() + + # Rerunning find_issues() and report() with the same default parameters should not change the number of issues + large_lab.find_issues( + features=features, pred_probs=pred_probs, knn_graph=knn_graph, issue_types=issue_types + ) + with contextlib.redirect_stdout(io.StringIO()) as f: + large_lab.report() + second_report = f.getvalue() + pd.testing.assert_frame_equal(large_lab.issues, issues) + pd.testing.assert_frame_equal(large_lab.issue_summary, issue_summary) + assert first_report == second_report + + @pytest.mark.parametrize("k", [2, 3]) + @pytest.mark.parametrize("metric", ["euclidean", "cosine"]) + def test_find_issues_with_custom_hyperparams(self, lab, pred_probs, k, metric): + dataset_size = lab.get_info("statistics")["num_examples"] + embedding_size = 2 + mock_embeddings = np.random.rand(dataset_size, embedding_size) + + knn = NearestNeighbors(n_neighbors=k, metric=metric) + issue_types = {"outlier": {"knn": knn}} + assert lab.get_info("statistics").get("weighted_knn_graph") is None + lab.find_issues( + pred_probs=pred_probs, + features=mock_embeddings, + issue_types=issue_types, + ) + assert lab.info["outlier"]["k"] == k + statistics = lab.get_info("statistics") + assert statistics["knn_metric"] == metric + knn_graph = statistics["weighted_knn_graph"] + assert isinstance(knn_graph, csr_matrix) + assert knn_graph.shape == (dataset_size, dataset_size) + assert knn_graph.nnz == dataset_size * k + + # Mock the lab.issues dataframe to have some pre-existing issues + def test_update_issues(self, lab, pred_probs, monkeypatch): + """If there are pre-existing issues in the lab, + find_issues should add columns to the issues dataframe for each example. + """ + mock_issues = pd.DataFrame( + { + "is_foo_issue": [False, True, False, False, False], + "foo_score": [0.6, 0.8, 0.7, 0.7, 0.8], + } + ) + monkeypatch.setattr(lab, "issues", mock_issues) + mock_issue_summary = pd.DataFrame( + { + "issue_type": ["foo"], + "score": [0.72], + "num_issues": [1], + } + ) + monkeypatch.setattr(lab, "issue_summary", mock_issue_summary) + + lab.find_issues(pred_probs=pred_probs, issue_types={"label": {}}) + # Check that the issues dataframe has the right columns + expected_issues_df = pd.DataFrame( + { + "is_foo_issue": mock_issues.is_foo_issue, + "foo_score": mock_issues.foo_score, + "is_label_issue": [False, False, False, False, False], + "label_score": [0.95071431, 0.15601864, 0.60111501, 0.70807258, 0.18182497], + } + ) + pd.testing.assert_frame_equal(lab.issues, expected_issues_df, check_exact=False) + + expected_issue_summary_df = pd.DataFrame( + { + "issue_type": ["foo", "label"], + "score": [0.72, 0.6], + "num_issues": [1, 0], + } + ) + pd.testing.assert_frame_equal( + lab.issue_summary, expected_issue_summary_df, check_exact=False + ) + + def test_save(self, lab, tmp_path, monkeypatch): + """Test that the save and load methods work.""" + lab.save(tmp_path, force=True) + assert tmp_path.exists(), "Save directory was not created" + assert (tmp_path / "data").is_dir(), "Data directory was not saved" + assert (tmp_path / "issues.csv").exists(), "Issues file was not saved" + assert (tmp_path / "summary.csv").exists(), "Issue summary file was not saved" + assert (tmp_path / "datalab.pkl").exists(), "Datalab file was not saved" + + # Mock the issues dataframe + mock_issues = pd.DataFrame( + { + "is_foo_issue": [False, True, False, False, False], + "foo_score": [0.6, 0.8, 0.7, 0.7, 0.8], + } + ) + monkeypatch.setattr(lab, "issues", mock_issues) + + # Mock the issue summary dataframe + mock_issue_summary = pd.DataFrame( + { + "issue_type": ["foo"], + "score": [0.72], + } + ) + monkeypatch.setattr(lab, "issue_summary", mock_issue_summary) + lab.save(tmp_path, force=True) + assert (tmp_path / "issues.csv").exists(), "Issues file was not saved" + assert (tmp_path / "summary.csv").exists(), "Issue summary file was not saved" + + # Save works in an arbitrary directory, that should be created if it doesn't exist + new_dir = tmp_path / "subdir" + assert not new_dir.exists(), "Directory should not exist" + lab.save(new_dir) + assert new_dir.exists(), "Directory was not created" + + def test_pickle(self, lab, tmp_path): + """Test that the class can be pickled.""" + pickle_file = os.path.join(tmp_path, "lab.pkl") + with open(pickle_file, "wb") as f: + pickle.dump(lab, f) + with open(pickle_file, "rb") as f: + lab2 = pickle.load(f) + + assert lab2.label_name == "star" + + def test_load(self, lab, tmp_path, dataset, monkeypatch): + """Test that the save and load methods work.""" + + # Mock the issues dataframe + mock_issues = pd.DataFrame( + { + "is_foo_issue": [False, True, False, False, False], + "foo_score": [0.6, 0.8, 0.7, 0.7, 0.8], + } + ) + monkeypatch.setattr(lab, "issues", mock_issues) + + # Mock the issue summary dataframe + mock_issue_summary = pd.DataFrame( + { + "issue_type": ["foo"], + "score": [0.72], + } + ) + monkeypatch.setattr(lab, "issue_summary", mock_issue_summary) + + lab.save(tmp_path, force=True) + + loaded_lab = Datalab.load(tmp_path) + data = lab._data + loaded_data = loaded_lab._data + assert loaded_data == data + assert loaded_lab.info == lab.info + pd.testing.assert_frame_equal(loaded_lab.issues, mock_issues) + pd.testing.assert_frame_equal(loaded_lab.issue_summary, mock_issue_summary) + + # Load accepts a `Dataset`. + loaded_lab = Datalab.load(tmp_path, data=dataset) + assert loaded_lab.data._data == dataset.data + + # Misaligned dataset raises a ValueError + with pytest.raises(ValueError) as excinfo: + Datalab.load(tmp_path, data=dataset.shard(2, 0)) + expected_error_msg = "Length of data (2) does not match length of labels (5)" + assert expected_error_msg == str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + Datalab.load(tmp_path, data=dataset.shuffle()) + expected_error_msg = ( + "Data has been modified since Lab was saved. Cannot load Lab with modified data." + ) + assert expected_error_msg == str(excinfo.value) + + def test_failed_issue_managers(self, lab, monkeypatch): + """Test that a failed issue manager will not be added to the Datalab instance after + the call to `find_issues`.""" + mock_issue_types = {"erroneous_issue_type": {}} + + mock_issue_manager = Mock() + mock_issue_manager.issue_name = "erroneous_issue_type" + mock_issue_manager.find_issues.side_effect = ValueError("Some error") + + class MockIssueManagerFactory: + @staticmethod + def from_list(*args, **kwargs): + return [mock_issue_manager] + + monkeypatch.setattr( + "cleanlab.datalab.issue_finder._IssueManagerFactory", MockIssueManagerFactory + ) + + assert lab.issues.empty + with patch("builtins.print") as mock_print: + lab.find_issues(issue_types=mock_issue_types) + for expected_msg_substr in [ + "Error in", + "Audit complete", + "Failed to check for these issue types: ", + ]: + assert any(expected_msg_substr in call[0][0] for call in mock_print.call_args_list) + + assert lab.issues.empty + + def test_report(self, lab): + class MockReporter: + def __init__(self, *args, **kwargs): + self.verbosity = kwargs.get("verbosity", None) + assert self.verbosity is not None, "Reporter should be initialized with verbosity" + + def report(self, *args, **kwargs) -> None: + print( + f"Report with verbosity={self.verbosity} and k={kwargs.get('num_examples', 5)}" + ) + + with patch("cleanlab.datalab.datalab.Reporter", new=MockReporter): + # Call report with no arguments, test that it prints the report + with patch("builtins.print") as mock_print: + lab.report(verbosity=0) + mock_print.assert_called_once_with("Report with verbosity=0 and k=5") + mock_print.reset_mock() + lab.report(num_examples=10, verbosity=3) + mock_print.assert_called_once_with("Report with verbosity=3 and k=10") + mock_print.reset_mock() + lab.report() + mock_print.assert_called_once_with("Report with verbosity=1 and k=5") + + +class TestDatalabUsingKNNGraph: + """The Datalab class can accept a `knn_graph` argument to `find_issues` that should + be used instead of computing a new one from the `features` argument.""" + + @pytest.fixture + def data_tuple(self): + # from cleanlab.datalab.datalab import Datalab + np.random.seed(SEED) + N = 10 + data = {"label": np.random.randint(0, 2, size=N)} + features = np.random.rand(N, 5) + knn_graph = ( + NearestNeighbors(n_neighbors=3, metric="cosine") + .fit(features) + .kneighbors_graph(mode="distance") + ) + return Datalab(data=data, label_name="label"), knn_graph, features + + def test_knn_graph(self, data_tuple): + """Test that the `knn_graph` argument to `find_issues` is used instead of computing a new + one from the `features` argument.""" + lab, knn_graph, _ = data_tuple + assert lab.get_info("statistics").get("weighted_knn_graph") is None + lab.find_issues(knn_graph=knn_graph) + knn_graph_stats = lab.get_info("statistics").get("weighted_knn_graph") + np.testing.assert_array_equal(knn_graph_stats.toarray(), knn_graph.toarray()) + + assert lab.get_info("statistics").get("knn_metric") is None + + def test_features_and_knn_graph(self, data_tuple): + """Test that the `knn_graph` argument to `find_issues` is used instead of computing a new + one from the `features` argument.""" + lab, knn_graph, features = data_tuple + k = 4 + lab.find_issues(knn_graph=knn_graph, features=features, issue_types={"outlier": {"k": k}}) + knn_graph_stats = lab.get_info("statistics").get("weighted_knn_graph") + assert knn_graph_stats.nnz == k * len( + lab.data + ), f"Expected {k * len(lab.data)} nnz, got {knn_graph_stats.nnz}" + three_nn_dists = knn_graph_stats.data.reshape(len(lab.data), k)[:, :3] + knn_graph_three_nn_dists = knn_graph.data.reshape(len(lab.data), k - 1) + np.testing.assert_array_equal(three_nn_dists, knn_graph_three_nn_dists) + assert lab.get_info("statistics").get("knn_metric") == "cosine" + + def test_without_features_or_knn_graph(self, data_tuple): + """Test that the `knn_graph` argument to `find_issues` is used instead of computing a new + one from the `features` argument.""" + lab, _, _ = data_tuple + + # Test that a warning is raised + with pytest.warns(UserWarning) as record: + lab.find_issues() + + assert len(record) == 2 + assert "No arguments were passed to find_issues." == str(record[0].message) + assert "No issue check performed." == str(record[1].message) + assert lab.issues.empty # No columns should be added to the issues dataframe + + +class TestDatalabIssueManagerInteraction: + """The Datalab class should integrate with the IssueManager class correctly. + + Tests include: + - Make sure a custom manager needs to be registered to work with Datalab + - Make sure that `find_issues()` with different affects the outcome (e.g. `Datalab.issues`) + differently depending on the issue manager. + """ + + def test_custom_issue_manager_not_registered(self, lab): + """Test that a custom issue manager that is not registered will not be used.""" + # Mock registry dictionary + mock_registry = MagicMock() + mock_registry.__getitem__.side_effect = KeyError("issue type not registered") + + with patch("cleanlab.datalab.factory.REGISTRY", mock_registry): + with pytest.raises(ValueError) as excinfo: + lab.find_issues(issue_types={"custom_issue": {}}) + + assert "issue type not registered" in str(excinfo.value) + + assert mock_registry.__getitem__.called_once_with("custom_issue") + + assert lab.issues.empty + assert lab.issue_summary.empty + + def test_custom_issue_manager_registered(self, lab, custom_issue_manager): + """Test that a custom issue manager that is registered will be used.""" + from cleanlab.datalab.factory import register + + register(custom_issue_manager) + + assert lab.issues.empty + assert lab.issue_summary.empty + + lab.find_issues(issue_types={"custom_issue": {}}) + + expected_is_custom_issue_issue = [False, True] + [False] * 3 + expected_custom_issue_score = [1 / 1, 0 / 2, 1 / 3, 2 / 4, 3 / 5] + expected_issues = pd.DataFrame( + { + "is_custom_issue_issue": expected_is_custom_issue_issue, + "custom_issue_score": expected_custom_issue_score, + } + ) + assert pd.testing.assert_frame_equal(lab.issues, expected_issues) is None + + def test_find_issues_for_custom_issue_manager_with_custom_kwarg( + self, lab, custom_issue_manager + ): + """Test that a custom issue manager that is registered will be used.""" + from cleanlab.datalab.factory import register + + register(custom_issue_manager) + + assert lab.issues.empty + assert lab.issue_summary.empty + + lab.find_issues(issue_types={"custom_issue": {"custom_argument": 3}}) + + expected_is_custom_issue_issue = [False, False, False, True, False] + expected_custom_issue_score = [3 / 3, 2 / 4, 1 / 5, 0 / 6, 1 / 7] + expected_issues = pd.DataFrame( + { + "is_custom_issue_issue": expected_is_custom_issue_issue, + "custom_issue_score": expected_custom_issue_score, + } + ) + assert pd.testing.assert_frame_equal(lab.issues, expected_issues) is None + + # Clean up registry + from cleanlab.datalab.factory import REGISTRY + + REGISTRY.pop(custom_issue_manager.issue_name) + + +@pytest.mark.parametrize( + "find_issues_kwargs", + [ + ({"pred_probs": np.random.rand(3, 2)}), + ({"features": np.random.rand(3, 2)}), + ({"pred_probs": np.random.rand(3, 2), "features": np.random.rand(6, 2)}), + ], + ids=["pred_probs", "features", "pred_probs and features"], +) +def test_report_for_outlier_issues_via_pred_probs(find_issues_kwargs): + data = {"labels": [0, 1, 0]} + lab = Datalab(data=data, label_name="labels") + find_issues_kwargs["issue_types"] = {"outlier": {"k": 1}} + lab.find_issues(**find_issues_kwargs) + + reporter = Reporter(lab.data_issues, verbosity=0, include_description=False) + report = reporter.get_report(num_examples=3) + assert report, "Report should not be empty" + + +def test_near_duplicates_reuses_knn_graph(): + """'outlier' and 'near_duplicate' issues both require a KNN graph. + This test ensures that the KNN graph is only computed once. + E.g. if outlier is called first, and then near_duplicate can reuse the + resulting graph. + """ + N = 3000 + num_features = 1000 + k = 20 + data = {"labels": np.random.randint(0, 2, size=N)} + + np.random.seed(SEED) + features = np.random.rand(N, num_features) + + # Run 1: only near_duplicate + lab = Datalab(data=data, label_name="labels") + find_issues_kwargs = {"issue_types": {"near_duplicate": {"k": k}}} + time_only_near_duplicates = timeit.timeit( + lambda: lab.find_issues(features=features, **find_issues_kwargs), + number=1, + ) + + # Run 2: near_duplicate and outlier with same k + lab = Datalab(data=data, label_name="labels") + # Outliers need more neighbors, so this should be slower, so the graph will be computed twice + find_issues_kwargs = { + "issue_types": {"near_duplicate": {"k": k}, "outlier": {"k": 2 * k}}, + } + time_near_duplicates_and_outlier = timeit.timeit( + lambda: lab.find_issues(features=features, **find_issues_kwargs), + number=1, + ) + + # Run 3: Same Datalab instance with same issues, but in different order + find_issues_kwargs = { + "issue_types": {"outlier": {"k": 2 * k}, "near_duplicate": {"k": k}}, + } + time_outliers_before_near_duplicates = timeit.timeit( + lambda: lab.find_issues(features=features, **find_issues_kwargs), + number=1, + ) + + # Run 2 does an extra check, so it should be slower + assert time_only_near_duplicates < time_near_duplicates_and_outlier, ( + "Run 2 should be slower because it does an extra check " + "for outliers, which requires a KNN graph." + ) + + # Run 3 should be faster because it reuses the KNN graph from Run 2 + # in both issue checks + assert ( + time_outliers_before_near_duplicates < time_near_duplicates_and_outlier + ), "KNN graph reuse should make this run of find_issues faster." + + +class TestDatalabFindNonIIDIssues: + """This class focuses on testing the end-to-end functionality of calling Datalab.find_issues() + only for non-IID issues. The tests in this class are not meant to test the underlying + functionality of the non-IID issue finders themselves, but rather to test that the + Datalab.find_issues() method correctly calls the non-IID issue finders and results are consistent. + """ + + @pytest.fixture + def random_embeddings(self): + np.random.seed(SEED) + return np.random.rand(100, 10) + + @pytest.fixture + def sorted_embeddings(self): + np.random.seed(SEED) + n_samples = 1000 + + # Stack features to create a 3D dataset + x = np.linspace(0, 4 * np.pi, n_samples) + y = np.sin(x) + np.random.normal(0, 0.1, n_samples) + z = np.cos(x) + np.random.normal(0, 0.1, n_samples) + return np.column_stack((x, y, z)) + + def test_find_non_iid_issues(self, random_embeddings): + data = {"labels": [0, 1, 0]} + lab = Datalab(data=data, label_name="labels") + lab.find_issues(features=random_embeddings, issue_types={"non_iid": {}}) + summary = lab.get_issue_summary() + assert ["non_iid"] == summary["issue_type"].values + assert summary["score"].values[0] > 0.05 + assert lab.get_issues()["is_non_iid_issue"].sum() == 0 + + def test_find_non_iid_issues_sorted(self, sorted_embeddings): + data = {"labels": [0, 1, 0]} + lab = Datalab(data=data, label_name="labels") + lab.find_issues(features=sorted_embeddings, issue_types={"non_iid": {}}) + summary = lab.get_issue_summary() + assert ["non_iid"] == summary["issue_type"].values + assert summary["score"].values[0] == 0 + assert lab.get_issues()["is_non_iid_issue"].sum() == 1 + + def test_incremental_search(self, sorted_embeddings): + data = {"labels": [0, 1, 0]} + lab = Datalab(data=data, label_name="labels") + lab.find_issues(features=sorted_embeddings) + summary = lab.get_issue_summary() + assert len(summary) == 3 + lab.find_issues(features=sorted_embeddings, issue_types={"non_iid": {}}) + summary = lab.get_issue_summary() + assert len(summary) == 3 + assert "non_iid" in summary["issue_type"].values + non_iid_summary = lab.get_issue_summary("non_iid") + assert non_iid_summary["score"].values[0] == 0 + assert non_iid_summary["num_issues"].values[0] == 1 + + +class TestDatalabFindLabelIssues: + @pytest.fixture + def random_embeddings(self): + np.random.seed(SEED) + return np.random.rand(100, 10) + + @pytest.fixture + def pred_probs(self): + np.random.seed(SEED) + pred_probs_array = np.random.rand(100, 2) + return pred_probs_array / pred_probs_array.sum(axis=1, keepdims=True) + + def test_incremental_search(self, pred_probs, random_embeddings): + data = {"labels": np.random.randint(0, 2, 100)} + lab = Datalab(data=data, label_name="labels") + lab.find_issues(features=random_embeddings) + summary = lab.get_issue_summary() + assert len(summary) == 3 + assert "label" not in summary["issue_type"].values + lab.find_issues(pred_probs=pred_probs, issue_types={"label": {}}) + summary = lab.get_issue_summary() + assert len(summary) == 4 + assert "label" in summary["issue_type"].values + label_summary = lab.get_issue_summary("label") + assert label_summary["num_issues"].values[0] > 0 + + +class TestDatalabFindOutlierIssues: + @pytest.fixture + def random_embeddings(self): + np.random.seed(SEED) + X = np.random.rand(100, 10) + X[-1] += 10 * np.random.rand(10) + return np.random.rand(100, 10) + + @pytest.fixture + def pred_probs(self): + np.random.seed(SEED) + pred_probs_array = np.random.rand(100, 2) + return pred_probs_array / pred_probs_array.sum(axis=1, keepdims=True) + + def test_incremental_search(self, pred_probs, random_embeddings): + data = {"labels": np.random.randint(0, 2, 100)} + lab = Datalab(data=data, label_name="labels") + lab.find_issues(pred_probs=pred_probs, issue_types={"label": {}}) + summary = lab.get_issue_summary() + assert len(summary) == 1 + assert "outlier" not in summary["issue_type"].values + lab.find_issues(features=random_embeddings, issue_types={"outlier": {}}) + summary = lab.get_issue_summary() + assert len(summary) == 2 + assert "outlier" in summary["issue_type"].values + outlier_summary = lab.get_issue_summary("outlier") + assert outlier_summary["num_issues"].values[0] > 0 + + +class TestDatalabFindNearDuplicateIssues: + @pytest.fixture + def random_embeddings(self): + np.random.seed(SEED) + X = np.random.rand(100, 10) + X[-1] = X[-1] * -1 + X[-2] = X[-1] + 0.0001 * np.random.rand(10) + return X + + @pytest.fixture + def pred_probs(self): + np.random.seed(SEED) + pred_probs_array = np.random.rand(100, 2) + return pred_probs_array / pred_probs_array.sum(axis=1, keepdims=True) + + def test_incremental_search(self, pred_probs, random_embeddings): + data = {"labels": np.random.randint(0, 2, 100)} + lab = Datalab(data=data, label_name="labels") + lab.find_issues(pred_probs=pred_probs, issue_types={"label": {}}) + summary = lab.get_issue_summary() + assert len(summary) == 1 + assert "near_duplicate" not in summary["issue_type"].values + lab.find_issues(features=random_embeddings, issue_types={"near_duplicate": {}}) + summary = lab.get_issue_summary() + assert len(summary) == 2 + assert "near_duplicate" in summary["issue_type"].values + near_duplicate_summary = lab.get_issue_summary("near_duplicate") + assert near_duplicate_summary["num_issues"].values[0] > 1 + + +class TestDatalabWithoutLabels: + num_examples = 100 + num_features = 10 + K = 2 + + @pytest.fixture + def features(self): + np.random.seed(SEED) + return np.random.rand(self.num_examples, self.num_features) + + @pytest.fixture + def pred_probs(self): + np.random.seed(SEED) + pred_probs_array = np.random.rand(self.num_examples, self.K) + return pred_probs_array / pred_probs_array.sum(axis=1, keepdims=True) + + @pytest.fixture + def lab(self, features): + return Datalab(data={"X": features}) + + @pytest.fixture + def labels(self): + np.random.seed(SEED) + return np.random.randint(0, self.K, self.num_examples) + + def test_init(self, lab, features): + assert np.array_equal(lab.data["X"], features) + assert np.array_equal(lab.labels, []) + + def test_find_issues(self, lab, features, pred_probs): + lab = Datalab(data={"X": features}) + lab.find_issues(pred_probs=pred_probs) + assert lab.issues.empty + + lab = Datalab(data={"X": features}) + lab.find_issues(features=features) + assert not lab.issues.empty + + def test_find_issues_features_works_with_and_without_labels(self, features, labels): + lab_without_labels = Datalab(data={"X": features}) + lab_without_labels.find_issues(features=features) + + lab_with_labels = Datalab(data={"X": features, "labels": labels}, label_name="labels") + lab_with_labels.find_issues(features=features) + + lab_without_label_name = Datalab(data={"X": features, "labels": labels}) + lab_without_label_name.find_issues(features=features) + + issues_without_labels = lab_without_labels.issues + issues_with_labels = lab_with_labels.issues + issues_without_label_name = lab_without_label_name.issues + + pd.testing.assert_frame_equal(issues_without_labels, issues_with_labels) + pd.testing.assert_frame_equal(issues_without_labels, issues_without_label_name) diff --git a/tests/datalab/test_factory.py b/tests/datalab/test_factory.py new file mode 100644 index 0000000000..dad1797a33 --- /dev/null +++ b/tests/datalab/test_factory.py @@ -0,0 +1,31 @@ +import pytest + +from cleanlab.datalab.factory import register, REGISTRY +from cleanlab import Datalab +from cleanlab.datalab.issue_manager.issue_manager import IssueManager + + +@pytest.fixture +def registry(): + return REGISTRY + + +def test_list_possible_issue_types(registry): + issue_types = Datalab.list_possible_issue_types() + assert isinstance(issue_types, list) + defaults = ["label", "outlier", "near_duplicate", "non_iid"] + assert set(issue_types) == set(defaults) + + test_key = "test_for_list_possible_issue_types" + + @register + class TestIssueManager(IssueManager): + issue_name = test_key + + issue_types = Datalab.list_possible_issue_types() + assert set(issue_types) == set( + defaults + [test_key] + ), "New issue type should be added to the list" + + # Clean up + del registry[test_key] diff --git a/tests/datalab/test_init.py b/tests/datalab/test_init.py new file mode 100644 index 0000000000..07f901b67b --- /dev/null +++ b/tests/datalab/test_init.py @@ -0,0 +1,16 @@ +import sys +import importlib +from unittest.mock import patch + + +def test_datalab_unavailable(): + with patch.dict(sys.modules, {"cleanlab.datalab.datalab": ImportError("Mocked ImportError")}): + # Reload the module to trigger the import statement + import cleanlab + + importlib.reload(cleanlab) + + assert cleanlab.Datalab.message == ( + "Datalab is not available due to missing dependencies. " + "To install Datalab, run `pip install 'cleanlab[datalab]'`." + ) diff --git a/tests/datalab/test_issue_finder.py b/tests/datalab/test_issue_finder.py new file mode 100644 index 0000000000..71500a0096 --- /dev/null +++ b/tests/datalab/test_issue_finder.py @@ -0,0 +1,83 @@ +import pytest +import numpy as np + +from cleanlab.datalab.issue_finder import IssueFinder + +from cleanlab import Datalab + + +class TestIssueFinder: + @pytest.fixture + def lab(self): + N = 30 + K = 2 + y = np.random.randint(0, K, size=N) + lab = Datalab(data={"y": y}, label_name="y") + return lab + + @pytest.fixture + def issue_finder(self, lab): + return IssueFinder(datalab=lab) + + def test_init(self, issue_finder): + assert issue_finder.verbosity == 1 + + def test_find_issues(self, issue_finder, lab): + N = len(lab.data) + K = lab.get_info("statistics")["num_classes"] + X = np.random.rand(N, 2) + pred_probs = np.random.rand(N, K) + pred_probs = pred_probs / pred_probs.sum(axis=1, keepdims=True) + + data_issues = lab.data_issues + assert data_issues.issues.empty + + issue_finder.find_issues( + features=X, + pred_probs=pred_probs, + ) + + assert not data_issues.issues.empty + + def test_validate_issue_types_dict(self, issue_finder, monkeypatch): + issue_types = { + "issue_type_1": {f"arg_{i}": f"value_{i}" for i in range(1, 3)}, + "issue_type_2": {f"arg_{i}": f"value_{i}" for i in range(1, 4)}, + } + defaults_dict = issue_types.copy() + + issue_types["issue_type_2"][ + "arg_2" + ] = "another_value_2" # Should be in default, but not affect the test + issue_types["issue_type_2"][ + "arg_4" + ] = "value_4" # Additional arg not in defaults should be allowed (ignored) + + with monkeypatch.context() as m: + m.setitem(issue_types, "issue_type_1", {}) + with pytest.raises(ValueError) as e: + issue_finder._validate_issue_types_dict(issue_types, defaults_dict) + assert all([string in str(e.value) for string in ["issue_type_1", "arg_1", "arg_2"]]) + + @pytest.mark.parametrize( + "defaults_dict", + [ + {"issue_type_1": {"arg_1": "default_value_1"}}, + ], + ) + @pytest.mark.parametrize( + "issue_types", + [{"issue_type_1": {"arg_1": "value_1", "arg_2": "value_2"}}, {"issue_type_1": {}}], + ) + def test_set_issue_types(self, issue_finder, issue_types, defaults_dict, monkeypatch): + """Test that the issue_types dict is set correctly.""" + with monkeypatch.context() as m: + # Mock the validation method to do nothing + m.setattr(issue_finder, "_validate_issue_types_dict", lambda x, y: None) + issue_types_copy = issue_finder._set_issue_types(issue_types, defaults_dict) + + # For each argument in issue_types missing from defaults_dict, it should be added to the defaults dict + for issue_type, args in issue_types.items(): + missing_args = set(args.keys()) - set(defaults_dict[issue_type].keys()) + for arg in missing_args: + assert issue_types_copy[issue_type][arg] == args[arg] diff --git a/tests/datalab/test_issue_manager.py b/tests/datalab/test_issue_manager.py new file mode 100644 index 0000000000..2e0d6d8434 --- /dev/null +++ b/tests/datalab/test_issue_manager.py @@ -0,0 +1,70 @@ +import numpy as np +import pandas as pd +import pytest + +from cleanlab.datalab.issue_manager import IssueManager +from cleanlab.datalab.factory import REGISTRY, register + + +class TestCustomIssueManager: + @pytest.mark.parametrize( + "score", + [0, 0.5, 1], + ids=["zero", "positive_float", "one"], + ) + def test_make_summary_with_score(self, custom_issue_manager, score): + summary = custom_issue_manager.make_summary(score=score) + + expected_summary = pd.DataFrame( + { + "issue_type": [custom_issue_manager.issue_name], + "score": [score], + } + ) + assert pd.testing.assert_frame_equal(summary, expected_summary) is None + + @pytest.mark.parametrize( + "score", + [-0.3, 1.5, np.nan, np.inf, -np.inf], + ids=["negative_float", "greater_than_one", "nan", "inf", "negative_inf"], + ) + def test_make_summary_invalid_score(self, custom_issue_manager, score): + with pytest.raises(ValueError): + custom_issue_manager.make_summary(score=score) + + +def test_register_custom_issue_manager(monkeypatch): + import io + import sys + + assert "foo" not in REGISTRY + + @register + class Foo(IssueManager): + issue_name = "foo" + + def find_issues(self): + pass + + assert "foo" in REGISTRY + assert REGISTRY["foo"] == Foo + + # Reregistering should overwrite the existing class, put print a warning + + monkeypatch.setattr("sys.stdout", io.StringIO()) + + @register + class NewFoo(IssueManager): + issue_name = "foo" + + def find_issues(self): + pass + + assert "foo" in REGISTRY + assert REGISTRY["foo"] == NewFoo + assert all( + [ + text in sys.stdout.getvalue() + for text in ["Warning: Overwriting existing issue manager foo with ", "NewFoo"] + ] + ), "Should print a warning" diff --git a/tests/datalab/test_report.py b/tests/datalab/test_report.py new file mode 100644 index 0000000000..d64c2fd8ed --- /dev/null +++ b/tests/datalab/test_report.py @@ -0,0 +1,97 @@ +import pytest +import numpy as np +import pandas as pd +from unittest.mock import Mock, patch + +from cleanlab.datalab.report import Reporter + +from cleanlab import Datalab + + +class TestReporter: + @pytest.fixture + def lab(self): + N = 30 + K = 2 + X = np.random.rand(N, K) + y = np.random.randint(0, K, size=N) + pred_probs = np.random.rand(N, K) + lab = Datalab(data={"y": y}, label_name="y") + lab.find_issues(features=X, pred_probs=pred_probs) + return lab + + @pytest.fixture + def data_issues(self, lab): + return lab.data_issues + + @pytest.fixture + def reporter(self, data_issues): + return Reporter(data_issues=data_issues) + + def test_init(self, reporter, data_issues): + assert reporter.data_issues == data_issues + assert reporter.verbosity == 1 + assert reporter.include_description == True + assert reporter.show_summary_score == False + + another_reporter = Reporter(data_issues=data_issues, verbosity=2) + assert another_reporter.verbosity == 2 + + def test_report(self, reporter): + """Test that the report method works. It just wraps the get_report method in a print + statement.""" + mock_get_report = Mock() + + with patch("builtins.print") as mock_print: # type: ignore + with patch.object(reporter, "get_report", mock_get_report): + reporter.report(num_examples=3) + mock_get_report.assert_called_with(num_examples=3) + mock_print.assert_called_with(mock_get_report.return_value) + + @pytest.mark.parametrize("include_description", [True, False]) + def test_get_report(self, reporter, data_issues, include_description, monkeypatch): + """Test that the report method works. Assuming we have two issue managers, each should add + their section to the report.""" + + mock_issue_manager = Mock() + mock_issue_manager.issue_name = "foo" + mock_issue_manager.report.return_value = "foo report" + + class MockIssueManagerFactory: + @staticmethod + def from_str(*args, **kwargs): + return mock_issue_manager + + monkeypatch.setattr("cleanlab.datalab.report._IssueManagerFactory", MockIssueManagerFactory) + mock_issues = pd.DataFrame( + { + "is_foo_issue": [False, True, False, False, False], + "foo_score": [0.6, 0.2, 0.7, 0.7, 0.8], + } + ) + monkeypatch.setattr(data_issues, "issues", mock_issues) + + mock_issue_summary = pd.DataFrame( + { + "issue_type": ["foo"], + "score": [0.6], + "num_issues": [1], + } + ) + + mock_info = {"foo": {"bar": "baz"}} + + monkeypatch.setattr(data_issues, "issue_summary", mock_issue_summary) + + reporter = Reporter( + data_issues=data_issues, verbosity=0, include_description=include_description + ) + monkeypatch.setattr(data_issues, "issues", mock_issues, raising=False) + monkeypatch.setattr(data_issues, "info", mock_info, raising=False) + + monkeypatch.setattr( + reporter, "_write_summary", lambda *args, **kwargs: "Here is a lab summary\n\n" + ) + report = reporter.get_report(num_examples=3) + expected_report = "\n\n".join(["Here is a lab summary", "foo report"]) + assert report == expected_report diff --git a/tests/test_classification.py b/tests/test_classification.py index 0f0277f098..ca99bccd91 100644 --- a/tests/test_classification.py +++ b/tests/test_classification.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -27,7 +27,11 @@ from cleanlab.benchmarking.noise_generation import generate_noise_matrix_from_trace from cleanlab.benchmarking.noise_generation import generate_noisy_labels from cleanlab.internal.latent_algebra import compute_inv_noise_matrix -from cleanlab.count import compute_confident_joint, estimate_cv_predicted_probabilities +from cleanlab.count import ( + compute_confident_joint, + estimate_cv_predicted_probabilities, + get_confident_thresholds, +) from cleanlab.filter import find_label_issues SEED = 1 @@ -776,6 +780,65 @@ def test_cj_in_find_label_issues_kwargs(filter_by, seed): assert num_issues[0] == num_issues[1] +def test_find_label_issues_uses_thresholds(): + X = DATA["X_train"] + labels = DATA["labels"] + pred_probs = estimate_cv_predicted_probabilities(X=X, labels=labels) + + confident_thresholds = get_confident_thresholds(labels=labels, pred_probs=pred_probs) + confident_joint = compute_confident_joint(labels=labels, pred_probs=pred_probs) + + # regular find label issues with no args + cl = CleanLearning() + label_issues_reg = cl.find_label_issues(labels=labels, pred_probs=pred_probs) + + # find label issues with specified confident thresholds + cl = CleanLearning() + label_issues_thres = cl.find_label_issues( + labels=labels, pred_probs=pred_probs, thresholds=confident_thresholds + ) + + # find label issues with specified confident joint + cl = CleanLearning( + find_label_issues_kwargs={ + "confident_joint": confident_joint, + } + ) + label_issues_cj = cl.find_label_issues(labels=labels, pred_probs=pred_probs) + + # the labels issues in above three calls should be the same + assert np.sum(label_issues_reg["is_label_issue"]) == np.sum( + label_issues_thres["is_label_issue"] + ) + assert np.sum(label_issues_reg["is_label_issue"]) == np.sum(label_issues_cj["is_label_issue"]) + + # find label issues with different specified confident thresholds + confident_thresholds_alt = np.full(pred_probs.shape[1], 0.25) + cl = CleanLearning() + label_issues_thres_alt = cl.find_label_issues( + labels=labels, pred_probs=pred_probs, thresholds=confident_thresholds_alt + ) + + # find label issues with different specified confident joint + confident_joint_alt = compute_confident_joint( + labels=labels, pred_probs=pred_probs, thresholds=confident_thresholds_alt + ) + cl = CleanLearning( + find_label_issues_kwargs={ + "confident_joint": confident_joint_alt, + } + ) + label_issues_cj_alt = cl.find_label_issues(labels=labels, pred_probs=pred_probs) + + # the number of issues for these 2 alt calls should be same as one another, but different from above 3 + assert np.sum(label_issues_thres_alt["is_label_issue"]) == np.sum( + label_issues_cj_alt["is_label_issue"] + ) + assert np.sum(label_issues_thres_alt["is_label_issue"]) != np.sum( + label_issues_reg["is_label_issue"] + ) + + def test_find_issues_missing_classes(): labels = np.array([0, 0, 2, 2]) pred_probs = np.array( diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 4532ed6534..d813854176 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -16,8 +16,11 @@ import requests import pytest +import hypothesis.extra.numpy as npst +import hypothesis.strategies as st import io import numpy as np +from hypothesis import given, settings from cleanlab.dataset import ( health_summary, find_overlapping_classes, @@ -445,6 +448,16 @@ def test_real_datasets(dataset_name): ) +@pytest.mark.parametrize("dataset_name", ["mnist"]) +def test_multilabel_error(dataset_name): + print("\n" + dataset_name.capitalize() + "\n") + class_names = eval(dataset_name) + pred_probs, labels = _get_pred_probs_labels_from_labelerrors_datasets(dataset_name) + # if this runs without issue no all four datasets, the test passes + with pytest.raises(ValueError) as e: + _ = find_overlapping_classes(labels=labels, pred_probs=pred_probs, multi_label=True) + + @pytest.mark.parametrize("asymmetric", [True, False]) @pytest.mark.parametrize("dataset_name", ["mnist", "imdb"]) def test_symmetry_df_size(asymmetric, dataset_name): @@ -492,3 +505,44 @@ def test_value_error_missing_num_examples_with_joint(use_num_examples, use_label joint=joint, num_examples=len(labels) if use_num_examples else None, ) + + +confident_joint_strategy = npst.arrays( + np.int32, + shape=npst.array_shapes(min_dims=2, max_dims=2, min_side=2, max_side=10), + elements=st.integers(min_value=0, max_value=int(1e6)), +).filter(lambda arr: arr.shape[0] == arr.shape[1]) + + +@pytest.mark.issue_651 +@given(confident_joint=confident_joint_strategy) +@settings(deadline=500) +def test_find_overlapping_classes_with_confident_joint(confident_joint): + # Setup + K = confident_joint.shape[0] + overlapping_classes = find_overlapping_classes(confident_joint=confident_joint) + + # Test that the output dataframe has the expected columns + expected_columns = [ + "Class Index A", + "Class Index B", + "Num Overlapping Examples", + "Joint Probability", + ] + assert set(overlapping_classes.columns) == set(expected_columns) + + # Class indices must be valid + assert overlapping_classes["Class Index A"].between(0, K - 1).all() + assert overlapping_classes["Class Index B"].between(0, K - 1).all() + + # Overlapping example count should be non-negative integers + assert (overlapping_classes["Num Overlapping Examples"] >= 0).all() + assert overlapping_classes["Num Overlapping Examples"].dtype == int + + # Joint probabilities should be between 0 and 1 + assert (overlapping_classes["Joint Probability"] >= 0).all() + assert (overlapping_classes["Joint Probability"] <= 1).all() + + # Joint probabilities sorted in descending order + if K > 2: + assert (overlapping_classes["Joint Probability"].diff()[1:] <= 0).all() diff --git a/tests/test_filter_count.py b/tests/test_filter_count.py index d7984534f7..1c126a0bf3 100644 --- a/tests/test_filter_count.py +++ b/tests/test_filter_count.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -14,6 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with cleanlab. If not, see . +import cleanlab.multilabel_classification.dataset from cleanlab import count, filter from cleanlab.count import ( get_confident_thresholds, @@ -24,12 +25,15 @@ from cleanlab.benchmarking.noise_generation import generate_noisy_labels from cleanlab.internal.util import value_counts from cleanlab.internal.multilabel_utils import int2onehot +from cleanlab.experimental.label_issues_batched import find_label_issues_batched import numpy as np import scipy import pytest from sklearn.multioutput import MultiOutputClassifier from sklearn.linear_model import LogisticRegression from sklearn.model_selection import cross_val_predict +from tempfile import mkdtemp +import os.path as path def make_data( @@ -493,6 +497,7 @@ def test_pruning_order_method(): @pytest.mark.parametrize("multi_label", [True, False]) +@pytest.mark.parametrize("use_dataset_function", [True, False]) @pytest.mark.parametrize( "filter_by", ["prune_by_noise_rate", "prune_by_class", "both", "confident_learning"] ) @@ -500,17 +505,37 @@ def test_pruning_order_method(): "return_indices_ranked_by", [None, "self_confidence", "normalized_margin", "confidence_weighted_entropy"], ) -def test_find_label_issues_multi_label(multi_label, filter_by, return_indices_ranked_by): +def test_find_label_issues_multi_label( + multi_label, use_dataset_function, filter_by, return_indices_ranked_by +): """Note: argmax_not_equal method is not compatible with multi_label == True""" dataset = multilabel_data if multi_label else data + if multi_label: + if use_dataset_function: + noise_idx = cleanlab.multilabel_classification.filter.find_label_issues( + labels=dataset["labels"], + pred_probs=dataset["pred_probs"], + filter_by=filter_by, + return_indices_ranked_by=return_indices_ranked_by, + ) + else: + with pytest.warns(DeprecationWarning): + noise_idx = filter.find_label_issues( + labels=dataset["labels"], + pred_probs=dataset["pred_probs"], + filter_by=filter_by, + multi_label=multi_label, + return_indices_ranked_by=return_indices_ranked_by, + ) + else: + noise_idx = filter.find_label_issues( + labels=dataset["labels"], + pred_probs=dataset["pred_probs"], + filter_by=filter_by, + multi_label=multi_label, + return_indices_ranked_by=return_indices_ranked_by, + ) - noise_idx = filter.find_label_issues( - labels=dataset["labels"], - pred_probs=dataset["pred_probs"], - filter_by=filter_by, - multi_label=multi_label, - return_indices_ranked_by=return_indices_ranked_by, - ) if return_indices_ranked_by is not None: noise_bool = np.zeros(len(dataset["labels"])).astype(bool) noise_bool[noise_idx] = True @@ -715,22 +740,74 @@ def test_find_label_issue_filters_match_origin_functions(): assert "not supported" in str(e) -@pytest.mark.parametrize("confident_joint", [None, True]) -def test_num_label_issues(confident_joint): - cj_calibrated_off_diag_sum = data["cj"].sum() - data["cj"].trace() - n = count.num_label_issues( +def test_num_label_issues_different_estimation_types(): + # these numbers are hardcoded as data[] does not create a difference in both functions + y = np.array([0, 1, 1, 1, 1, 0, 0, 1, 0]) + pred_probs = np.array( + [ + [0.7110397298505661, 0.2889602701494339], + [0.6367131487519773, 0.36328685124802274], + [0.7571834730987641, 0.24281652690123584], + [0.6394163729473307, 0.3605836270526695], + [0.5853684039196656, 0.4146315960803345], + [0.6675968116482668, 0.33240318835173316], + [0.7240647829106976, 0.2759352170893023], + [0.740474240697777, 0.25952575930222266], + [0.7148252196621883, 0.28517478033781196], + ] + ) + + n3 = count.num_label_issues( + labels=y, + pred_probs=pred_probs, + estimation_method="off_diagonal_calibrated", + ) + + n2 = count.num_label_issues( + labels=y, + pred_probs=pred_probs, + estimation_method="off_diagonal", + ) + + f2 = filter.find_label_issues(labels=y, pred_probs=pred_probs, filter_by="confident_learning") + + assert np.sum(f2) == n2 + assert n3 != n2 + + +def test_find_label_issues_same_value(): + f1 = filter.find_label_issues( labels=data["labels"], pred_probs=data["pred_probs"], - confident_joint=data["cj"], - estimation_method="off_diagonal", - ) # data["cj"] is already calibrated and estimation method does not do extra calibration + filter_by="confident_learning", + ) - n1 = count.num_label_issues( + f2 = filter.find_label_issues( + labels=data["labels"], + pred_probs=data["pred_probs"], + filter_by="low_self_confidence", + ) + + f3 = filter.find_label_issues( + labels=data["labels"], + pred_probs=data["pred_probs"], + filter_by="low_normalized_margin", + ) + + assert np.sum(f1) == np.sum(f2) + assert np.sum(f2) == np.sum(f3) + + +@pytest.mark.filterwarnings() +def test_num_label_issues(): + cj_calibrated_off_diag_sum = data["cj"].sum() - data["cj"].trace() + + n1 = count.num_label_issues( # should throw warning as cj is passed in but also recalculated labels=data["labels"], pred_probs=data["pred_probs"], confident_joint=data["cj"], estimation_method="off_diagonal_calibrated", - ) # data["cj"] is already calibrated but recalibrating it should not change the values + ) n2 = count.num_label_issues( labels=data["labels"], @@ -738,21 +815,47 @@ def test_num_label_issues(confident_joint): estimation_method="off_diagonal_calibrated", ) # this should calculate and calibrate the confident joint into same matrix as data["cj"] - # data["cj"] is already calibrated and estimation method does not do extra calibration - assert n == cj_calibrated_off_diag_sum - # data["cj"] is already calibrated but recalibrating it should not change the values - assert n == n1 + n_custom = count.num_label_issues( + labels=data["labels"], + pred_probs=data["pred_probs"], + confident_joint=data["cj"], + estimation_method="off_diagonal_custom", + ) + + ones_joint = np.ones_like(data["cj"]) + n_custom_bad = count.num_label_issues( + labels=data["labels"], + pred_probs=data["pred_probs"], + confident_joint=ones_joint, + estimation_method="off_diagonal_custom", + ) + + # data["cj"] is already calibrated and recalibrating it should not change the values + assert n2 == cj_calibrated_off_diag_sum # should calculate and calibrate the confident joint into same matrix as data["cj"] - assert n == n2 + assert n1 == n2 + # estimation_method='off_diagonal_custom' should use the passed in confident joint correctly + assert n_custom == n1 + assert n_custom_bad != n1 - f = filter.find_label_issues( + f = filter.find_label_issues( # this should throw warning since cj passed in and filter by confident_learning labels=data["labels"], pred_probs=data["pred_probs"], confident_joint=data["cj"] ) assert sum(f) == 35 - f1 = filter.find_label_issues( - labels=data["labels"], pred_probs=data["pred_probs"], filter_by="confident_learning" + f1 = filter.find_label_issues( # this should throw warning since cj passed in and filter by confident_learning + labels=data["labels"], + pred_probs=data["pred_probs"], + filter_by="confident_learning", + confident_joint=data["cj"], + ) + + n = count.num_label_issues( # should throw warning as cj is passed in but also recalculated + labels=data["labels"], + pred_probs=data["pred_probs"], + confident_joint=data["cj"], + estimation_method="off_diagonal", ) n3 = count.num_label_issues( @@ -761,6 +864,7 @@ def test_num_label_issues(confident_joint): ) assert sum(f1) == n3 # values should be equivalent for `filter_by='confident_learning'` + assert n == n3 # passing in cj should not affect calculation # check wrong estimation_method throws ValueError try: @@ -778,6 +882,22 @@ def test_num_label_issues(confident_joint): estimation_method="not_a_real_method", ) + # check not passing in cj with estimation_method_custom throws ValueError + try: + count.num_label_issues( + labels=data["labels"], + pred_probs=data["pred_probs"], + estimation_method="off_diagonal_custom", + ) + except Exception as e: + assert "you need to provide pre-calculated" in str(e) + with pytest.raises(ValueError) as e: + count.num_label_issues( + labels=data["labels"], + pred_probs=data["pred_probs"], + estimation_method="off_diagonal_custom", + ) + @pytest.mark.parametrize("confident_joint", [None, True]) def test_num_label_issues_multilabel(confident_joint): @@ -793,11 +913,112 @@ def test_num_label_issues_multilabel(confident_joint): labels=dataset["labels"], pred_probs=dataset["pred_probs"], confident_joint=dataset["cj"] if confident_joint else None, + filter_by="confident_learning", multi_label=True, ) assert sum(f) == n +def test_batched_label_issues(): + f1 = filter.find_label_issues( + labels=data["labels"], + pred_probs=data["pred_probs"], + return_indices_ranked_by="self_confidence", + filter_by="low_self_confidence", + ) + f2 = find_label_issues_batched( + labels=data["labels"], + pred_probs=data["pred_probs"], + batch_size=int(len(data["labels"]) / 4.0), + ) + f3 = find_label_issues_batched( + labels=data["labels"], + pred_probs=data["pred_probs"], + batch_size=int(len(data["labels"]) / 2.0), + n_jobs=None, + ) + f4 = find_label_issues_batched( + labels=data["labels"], + pred_probs=data["pred_probs"], + batch_size=len(data["labels"]) + 100, + n_jobs=4, + ) + f5 = find_label_issues_batched( + labels=data["labels"], + pred_probs=data["pred_probs"], + batch_size=1, + ) + f_single = find_label_issues_batched( + labels=data["labels"], + pred_probs=data["pred_probs"], + batch_size=len(data["labels"]), + n_jobs=1, + ) + assert np.all(f4 == f5) + assert np.all(f4 == f3) + assert np.all(f4 == f2) + assert np.all(f_single == f4) + assert len(f2) == len(f1) + # check jaccard similarity: + intersection = len(list(set(f1).intersection(set(f2)))) + union = len(set(f1)) + len(set(f2)) - intersection + assert float(intersection) / union > 0.95 + n1 = count.num_label_issues( + labels=data["labels"], + pred_probs=data["pred_probs"], + estimation_method="off_diagonal_calibrated", + ) + quality_score_kwargs = {"method": "normalized_margin"} + num_issue_kwargs = {"estimation_method": "off_diagonal_calibrated"} + extra_args = { + "quality_score_kwargs": quality_score_kwargs, + "num_issue_kwargs": num_issue_kwargs, + } + f5 = find_label_issues_batched( + labels=data["labels"], + pred_probs=data["pred_probs"], + batch_size=int(len(data["labels"]) / 4.0), + **extra_args, + ) + f6 = find_label_issues_batched( + labels=data["labels"], + pred_probs=data["pred_probs"], + batch_size=int(len(data["labels"]) / 2.0), + n_jobs=None, + **extra_args, + ) + f7 = find_label_issues_batched( + labels=data["labels"], + pred_probs=data["pred_probs"], + batch_size=len(data["labels"]) + 100, + n_jobs=4, + **extra_args, + ) + f_single = find_label_issues_batched( + labels=data["labels"], + pred_probs=data["pred_probs"], + batch_size=len(data["labels"]), + n_jobs=1, + **extra_args, + ) + assert not np.array_equal(f5, f2) + assert np.all(f7 == f5) + assert np.all(f6 == f5) + assert np.all(f_single == f5) + assert np.abs(len(f5) - n1) < 2 + # Test batches loaded from file: + labels_file = path.join(mkdtemp(), "labels.npy") + pred_probs_file = path.join(mkdtemp(), "pred_probs.npy") + np.save(labels_file, data["labels"]) + np.save(pred_probs_file, data["pred_probs"]) + f8 = find_label_issues_batched( + labels_file=labels_file, + pred_probs_file=pred_probs_file, + batch_size=int(len(data["labels"]) / 4.0), + ) + assert np.all(f8 == f3) + + def test_issue_158(): # ref: https://github.com/cleanlab/cleanlab/issues/158 pred_probs = np.array( @@ -875,6 +1096,57 @@ def test_missing_classes(): assert all(filter.find_label_issues(labels, pred_probs, filter_by=fb) == issues) +@pytest.mark.filterwarnings("ignore:WARNING!") +def test_find_label_issues_match_multiprocessing(): + # minimal version of this test was run in test_missing_classes + # here testing with larger input matrices + + # test with ground truth labels: + n = 5000 # consider replacing this with larger value + # some past bugs observed only with larger sample-sizes like n=200000 + m = 100 + labels = np.ones(n, dtype=int) + labels[(n // 2) :] = 0 + pred_probs = np.zeros((n, 4)) + pred_probs[:, 0] = 0.95 + pred_probs[:, 1] = 0.05 + pred_probs[0, 0] = 0.94 + pred_probs[0, 1] = 0.06 + ground_truth = np.ones(n, dtype=bool) + ground_truth[(n // 2) :] = False + ground_truth[0] = False # leave one example for min_example_per_class + # TODO: consider also testing this line without psutil installed + issues = filter.find_label_issues(labels, pred_probs) + issues1 = filter.find_label_issues(labels, pred_probs, n_jobs=1) + issues2 = filter.find_label_issues(labels, pred_probs, n_jobs=2) + assert all(issues == ground_truth) + assert all(issues == issues1) + assert all(issues == issues2) + issues = filter.find_label_issues(labels, pred_probs, filter_by="prune_by_class") + issues1 = filter.find_label_issues(labels, pred_probs, n_jobs=1, filter_by="prune_by_class") + issues2 = filter.find_label_issues(labels, pred_probs, n_jobs=2, filter_by="prune_by_class") + assert all(issues == ground_truth) + assert all(issues == issues1) + assert all(issues == issues2) + + # test with random labels + normalize = np.random.randint(low=1, high=100, size=[n, m], dtype=np.uint8) + pred_probs = np.zeros((n, m)) + for i, col in enumerate(normalize): + pred_probs[i] = col / np.sum(col) + labels = np.repeat(np.arange(m), n // m) + issues = filter.find_label_issues(labels, pred_probs) + issues1 = filter.find_label_issues(labels, pred_probs, n_jobs=1) + issues2 = filter.find_label_issues(labels, pred_probs, n_jobs=2) + assert all(issues == issues1) + assert all(issues == issues2) + issues = filter.find_label_issues(labels, pred_probs, filter_by="prune_by_class") + issues1 = filter.find_label_issues(labels, pred_probs, n_jobs=1, filter_by="prune_by_class") + issues2 = filter.find_label_issues(labels, pred_probs, n_jobs=2, filter_by="prune_by_class") + assert all(issues == issues1) + assert all(issues == issues2) + + @pytest.mark.parametrize( "return_indices_ranked_by", [None, "self_confidence", "normalized_margin", "confidence_weighted_entropy"], @@ -1008,3 +1280,64 @@ def test_estimate_py_and_noise_matrices_missing_classes(): ] ) _ = estimate_py_and_noise_matrices_from_probabilities(labels, pred_probs3) + + +def test_low_filter_by_methods(): + dataset = data + num_issues = count.num_label_issues(dataset["labels"], dataset["pred_probs"]) + + # test filter by low_normalized_margin, check num issues is same as using count.num_label_issues + label_issues_nm = filter.find_label_issues( + dataset["labels"], dataset["pred_probs"], filter_by="low_normalized_margin" + ) + assert sum(label_issues_nm) == num_issues + + # test filter by low_self_confidence, check num issues is same as using count.num_label_issues + label_issues_sc = filter.find_label_issues( + dataset["labels"], + dataset["pred_probs"], + filter_by="low_self_confidence", + return_indices_ranked_by="normalized_margin", + ) + assert len(label_issues_sc) == num_issues + + label_issues_sc_sort = filter.find_label_issues( + dataset["labels"], + dataset["pred_probs"], + filter_by="low_self_confidence", + return_indices_ranked_by="confidence_weighted_entropy", + ) + assert set(label_issues_sc) == set(label_issues_sc_sort) + + +def test_low_filter_by_methods_multilabel(): + dataset = multilabel_data + num_issues = count.num_label_issues(dataset["labels"], dataset["pred_probs"], multi_label=True) + + # test filter by low_normalized_margin, check num issues is same as using count.num_label_issues + label_issues_nm = filter.find_label_issues( + dataset["labels"], + dataset["pred_probs"], + filter_by="low_normalized_margin", + multi_label=True, + ) + assert sum(label_issues_nm) == num_issues + + # test filter by low_self_confidence, check num issues is same as using count.num_label_issues + label_issues_sc = filter.find_label_issues( + dataset["labels"], + dataset["pred_probs"], + filter_by="low_self_confidence", + multi_label=True, + return_indices_ranked_by="confidence_weighted_entropy", + ) + assert len(label_issues_sc) == num_issues + + label_issues_sc_sort = filter.find_label_issues( + dataset["labels"], + dataset["pred_probs"], + filter_by="low_self_confidence", + multi_label=True, + return_indices_ranked_by="self_confidence", + ) + assert set(label_issues_sc) == set(label_issues_sc_sort) diff --git a/tests/test_frameworks.py b/tests/test_frameworks.py index 9d9cdadbdb..d14f9f0a0f 100644 --- a/tests/test_frameworks.py +++ b/tests/test_frameworks.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -15,8 +15,8 @@ # along with cleanlab. If not, see . """ -Scripts to test cleanlab usage with deep learning frameworks: -pytorch, skorch, tensorflow, keras +Scripts to test cleanlab usage with various ML frameworks: +pytorch, skorch, tensorflow, keras, fasttext """ import pytest @@ -27,6 +27,7 @@ import sys import os +import wget from copy import deepcopy import random import numpy as np @@ -38,9 +39,13 @@ import tensorflow as tf import torch import skorch +from sklearn.pipeline import Pipeline +from sklearn.preprocessing import StandardScaler +from sklearn.model_selection import GridSearchCV from cleanlab.classification import CleanLearning -from cleanlab.experimental.keras import KerasWrapperSequential, KerasWrapperModel +from cleanlab.models.keras import KerasWrapperSequential, KerasWrapperModel +from cleanlab.internal.util import format_labels def python_version_ok(): # tensorflow and torch do not play nice with older Python @@ -48,6 +53,11 @@ def python_version_ok(): # tensorflow and torch do not play nice with older Pyt return (version.major >= 3) and (version.minor >= 7) +def run_fasttext_test(): + # run test only if os enviroment is set of true and os is not Windows + return os.environ.get("TEST_FASTTEXT") == "true" and os.name != "nt" + + def dataset_w_errors(): num_classes = 2 num_features = 3 @@ -133,6 +143,8 @@ def test_tensorflow_sequential(batch_size, shuffle_config, data=DATA, hidden_uni ], ) + model.summary() + # Test base model works: model.fit( X=dataset_tf, @@ -151,10 +163,14 @@ def test_tensorflow_sequential(batch_size, shuffle_config, data=DATA, hidden_uni assert issue_indices == data["error_indices"] assert err < 1e-3 + # Test wrapper works with numpy array + cl = CleanLearning(model) + cl.fit(data["X"], data["y"]) + @pytest.mark.skipif("not python_version_ok()", reason="need at least python 3.7") @pytest.mark.parametrize("batch_size,shuffle_config", [(1, 0), (32, 0), (32, 1), (32, 2)]) -def test_tensorflow_functional(batch_size, shuffle_config, data=DATA, hidden_units=128): +def test_tensorflow_functional(batch_size, shuffle_config, data=DATA, hidden_units=64): dataset_tf = tf.data.Dataset.from_tensor_slices((data["X"], data["y"])) if shuffle_config == 0: # proper shuffling for SGD dataset_shuffled = dataset_tf.shuffle(buffer_size=len(data["X"])) @@ -168,7 +184,7 @@ def test_tensorflow_functional(batch_size, shuffle_config, data=DATA, hidden_uni def make_model(num_features, num_classes): inputs = tf.keras.Input(shape=(num_features,)) - x = tf.keras.layers.Dense(64, activation="relu")(inputs) + x = tf.keras.layers.Dense(hidden_units, activation="relu")(inputs) outputs = tf.keras.layers.Dense(num_classes)(x) model = tf.keras.Model(inputs=inputs, outputs=outputs, name="test_model") @@ -179,6 +195,8 @@ def make_model(num_features, num_classes): model_kwargs={"num_features": data["num_features"], "num_classes": data["num_classes"]}, ) + model.summary() + # Test base model works: model.fit( X=dataset_tf, @@ -197,6 +215,10 @@ def make_model(num_features, num_classes): assert len(set(issue_indices) & set(data["error_indices"])) != 0 assert err < 1e-3 + # Test wrapper works with numpy array + cl = CleanLearning(model) + cl.fit(data["X"], data["y"]) + @pytest.mark.skipif("not python_version_ok()", reason="need at least python 3.7") @pytest.mark.parametrize("batch_size", [1, 32]) @@ -218,6 +240,66 @@ def test_tensorflow_rarelabel(batch_size, data=DATA_RARE_LABEL, hidden_units=8): preds = cl.predict(dataset_tf) +def test_keras_sklearn_compatability(data=DATA, hidden_units=32): + # test pipeline on Sequential API + model = KerasWrapperSequential( + [ + tf.keras.layers.Dense(128, input_shape=[data["num_features"]], activation="relu"), + tf.keras.layers.Dense(data["num_classes"]), + ], + ) + + pipeline = Pipeline([("scale", StandardScaler()), ("net", model)]) + pipeline.fit(data["X"], data["y"]) + preds = pipeline.predict(data["X"]) + + # test gridsearch on Sequential API + model = KerasWrapperSequential( + [ + tf.keras.layers.Dense( + hidden_units, input_shape=[data["num_features"]], activation="relu" + ), + tf.keras.layers.Dense(data["num_classes"]), + ], + ) + + params = {"batch_size": [32, 64], "epochs": [2, 3]} + gs = GridSearchCV( + model, params, refit=False, cv=3, verbose=2, scoring="accuracy", error_score="raise" + ) + gs.fit(data["X"], data["y"]) + + # test pipeline on functional API + def make_model(num_features, num_classes): + inputs = tf.keras.Input(shape=(num_features,)) + x = tf.keras.layers.Dense(64, activation="relu")(inputs) + outputs = tf.keras.layers.Dense(num_classes)(x) + model = tf.keras.Model(inputs=inputs, outputs=outputs, name="test_model") + + return model + + model = KerasWrapperModel( + make_model, + model_kwargs={"num_features": data["num_features"], "num_classes": data["num_classes"]}, + ) + + pipeline = Pipeline([("scale", StandardScaler()), ("net", model)]) + pipeline.fit(data["X"], data["y"]) + preds = pipeline.predict(data["X"]) + + # test gridsearch on Sequential API + model = KerasWrapperModel( + make_model, + model_kwargs={"num_features": data["num_features"], "num_classes": data["num_classes"]}, + ) + + params = {"batch_size": [32, 64], "epochs": [2, 3]} + gs = GridSearchCV( + model, params, refit=False, cv=3, verbose=2, scoring="accuracy", error_score="raise" + ) + gs.fit(data["X"], data["y"]) + + @pytest.mark.skipif("not python_version_ok()", reason="need at least python 3.7") def test_torch(data=DATA, hidden_units=128): dataset = torch.utils.data.TensorDataset( @@ -278,3 +360,56 @@ def forward(self, X): cl = CleanLearning(net) cl.fit(dataset, data["y"], clf_kwargs={"epochs": 2}) pred_probs = cl.predict(dataset) + + +# test fasttext only if not on windows and environment variable TEST_FASTTEXT has been set to "true" +@pytest.mark.skipif( + "not run_fasttext_test()", reason="fasttext is not easily pip install-able on windows" +) +def test_fasttext(): + from cleanlab.models.fasttext import FastTextClassifier, data_loader + + dir = "tests/fasttext_data" + if not os.path.isdir(dir): + os.makedirs(dir) + + try: + if not os.path.isfile("tests/fasttext_data/tweets_train.txt"): + wget.download( + "http://s.cleanlab.ai/tweets_fasttext/tweets_train.txt", "tests/fasttext_data" + ) + if not os.path.isfile("tests/fasttext_data/tweets_test.txt"): + wget.download( + "http://s.cleanlab.ai/tweets_fasttext/tweets_test.txt", "tests/fasttext_data" + ) + except: + raise RuntimeError( + "Download failed (potentially due to lack of internet connection or invalid url). " + "To skip this unittest, set the env variable TEST_FASTTEXT = false." + ) + + labels = np.ravel([x[0] for x in data_loader("tests/fasttext_data/tweets_train.txt")]) + labels = [lab[9:] for lab in labels] + labels, label_map = format_labels(labels) + X = np.array(range(len(labels))) + + # test basic fasttext methods + ftc = FastTextClassifier( + train_data_fn="tests/fasttext_data/tweets_train.txt", + test_data_fn="tests/fasttext_data/tweets_test.txt", + ) + ftc.fit() + pred_labels = ftc.predict() + pred_probs = ftc.predict_proba() + + # test CleanLearning + ftc = FastTextClassifier( + train_data_fn="tests/fasttext_data/tweets_train.txt", + test_data_fn="tests/fasttext_data/tweets_test.txt", + ) + cl = CleanLearning(ftc) + + issues = cl.find_label_issues(X=X, labels=labels) + cl.fit(X=X, labels=labels, label_issues=issues) + pred_labels = cl.predict() + pred_probs = cl.predict_proba() diff --git a/tests/test_latent_algebra.py b/tests/test_latent_algebra.py index bfa5e0f0ca..b55ea711e5 100644 --- a/tests/test_latent_algebra.py +++ b/tests/test_latent_algebra.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify diff --git a/tests/test_multiannotator.py b/tests/test_multiannotator.py index a428897870..74be98796b 100644 --- a/tests/test_multiannotator.py +++ b/tests/test_multiannotator.py @@ -434,6 +434,13 @@ def test_get_active_learning_scores(): assert len(active_learning_scores) == len(pred_probs) assert len(active_learning_scores_unlabeled) == 0 + # test case where only passing unlabeled examples + active_learning_scores, active_learning_scores_unlabeled = get_active_learning_scores( + pred_probs_unlabeled=pred_probs_unlabeled + ) + assert len(active_learning_scores) == 0 + assert len(active_learning_scores_unlabeled) == len(pred_probs_unlabeled) + # test case where number of classes do not match try: active_learning_scores, active_learning_scores_unlabeled = get_active_learning_scores( @@ -485,6 +492,13 @@ def test_get_active_learning_scores_ensemble(): assert len(active_learning_scores) == len(labels) assert len(active_learning_scores_unlabeled) == 0 + # test case where only passing unlabeled examples + active_learning_scores, active_learning_scores_unlabeled = get_active_learning_scores_ensemble( + pred_probs_unlabeled=pred_probs_unlabeled + ) + assert len(active_learning_scores) == 0 + assert len(active_learning_scores_unlabeled) == len(labels_unlabeled) + # test case where number of classes do not match try: ( @@ -640,9 +654,22 @@ def test_get_consensus_label(): [0.2, 0.4, 0.4], ] ) - consensus_label = get_majority_vote_label(labels_tiebreaks, pred_probs_tiebreaks) + # more tiebreak testing (without pred_probs + non-overlapping annotators) + labels_tiebreaks = np.array( + [ + [1, np.NaN, np.NaN, 2, np.NaN], + [np.NaN, 1, 0, np.NaN, np.NaN], + [np.NaN, np.NaN, 0, np.NaN, np.NaN], + [np.NaN, 2, np.NaN, np.NaN, np.NaN], + [2, np.NaN, 0, 2, np.NaN], + [np.NaN, np.NaN, np.NaN, 2, 1], + ] + ) + consensus_label = get_majority_vote_label(labels_tiebreaks) + assert all(consensus_label == np.array([1, 1, 0, 2, 2, 1])) + def test_impute_nonoverlaping_annotators(): labels = np.array( diff --git a/tests/test_multilabel_classification.py b/tests/test_multilabel_classification.py index 74ce2e361c..452dbc379f 100644 --- a/tests/test_multilabel_classification.py +++ b/tests/test_multilabel_classification.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -25,7 +25,15 @@ from cleanlab.internal import multilabel_scorer as ml_scorer from cleanlab.internal.multilabel_utils import stack_complement, get_onehot_num_classes, onehot2int -from cleanlab import multilabel_classification as multilabel_classfication +from cleanlab import multilabel_classification as ml_classification +from cleanlab.multilabel_classification.dataset import ( + common_multilabel_issues, + rank_classes_by_multilabel_quality, + overall_multilabel_health_score, + multilabel_health_summary, +) +from cleanlab.multilabel_classification.rank import get_label_quality_scores_per_class +from cleanlab.multilabel_classification import filter @pytest.fixture @@ -84,6 +92,41 @@ def pred_probs(): ) +@pytest.fixture +def pred_probs_multilabel(): + return np.array( + [ + [0.9, 0.1, 0.0, 0.4, 0.1], + [0.7, 0.8, 0.2, 0.3, 0.1], + [0.9, 0.8, 0.4, 0.2, 0.1], + [0.1, 0.1, 0.8, 0.3, 0.1], + [0.4, 0.5, 0.1, 0.1, 0.1], + [0.1, 0.1, 0.2, 0.1, 0.1], + [0.8, 0.1, 0.2, 0.1, 0.1], + ] + ) + + +@pytest.fixture +def labels_multilabel(): + return [[0], [0, 1], [0, 1], [2], [0, 2, 3], [], []] + + +@pytest.fixture +def data_multilabel(num_classes=5): + labels = [] + pred_probs = [] + for i in range(0, 100): + q = [0.1] * num_classes + pos = i % num_classes + labels.append([pos]) + if i > 90: + pos = (pos + 2) % num_classes + q[pos] = 0.9 + pred_probs.append(q) + return labels, np.array(pred_probs) + + @pytest.fixture def cv(): return sklearn.model_selection.StratifiedKFold( @@ -102,18 +145,18 @@ def dummy_features(labels): def test_public_label_quality_scores(labels, pred_probs): formatted_labels = onehot2int(labels) assert isinstance(formatted_labels, list) - scores1 = multilabel_classfication.get_label_quality_scores(formatted_labels, pred_probs) + scores1 = ml_classification.get_label_quality_scores(formatted_labels, pred_probs) assert len(scores1) == len(labels) assert (scores1 >= 0).all() and (scores1 <= 1).all() - scores2 = multilabel_classfication.get_label_quality_scores( + scores2 = ml_classification.get_label_quality_scores( formatted_labels, pred_probs, method="confidence_weighted_entropy" ) assert not np.isclose(scores1, scores2).all() - scores3 = multilabel_classfication.get_label_quality_scores( + scores3 = ml_classification.get_label_quality_scores( formatted_labels, pred_probs, adjust_pred_probs=True ) assert not np.isclose(scores1, scores3).all() - scores4 = multilabel_classfication.get_label_quality_scores( + scores4 = ml_classification.get_label_quality_scores( formatted_labels, pred_probs, method="normalized_margin", @@ -121,7 +164,7 @@ def test_public_label_quality_scores(labels, pred_probs): aggregator_kwargs={"method": "exponential_moving_average"}, ) assert not np.isclose(scores1, scores4).all() - scores5 = multilabel_classfication.get_label_quality_scores( + scores5 = ml_classification.get_label_quality_scores( formatted_labels, pred_probs, method="normalized_margin", @@ -129,7 +172,7 @@ def test_public_label_quality_scores(labels, pred_probs): aggregator_kwargs={"method": "softmin"}, ) assert not np.isclose(scores4, scores5).all() - scores6 = multilabel_classfication.get_label_quality_scores( + scores6 = ml_classification.get_label_quality_scores( formatted_labels, pred_probs, method="normalized_margin", @@ -137,7 +180,7 @@ def test_public_label_quality_scores(labels, pred_probs): aggregator_kwargs={"method": "softmin", "temperature": 0.002}, ) assert not np.isclose(scores5, scores6).all() - scores7 = multilabel_classfication.get_label_quality_scores( + scores7 = ml_classification.get_label_quality_scores( formatted_labels, pred_probs, method="normalized_margin", @@ -147,13 +190,13 @@ def test_public_label_quality_scores(labels, pred_probs): assert np.isclose(scores6, scores7, rtol=1e-3).all() with pytest.raises(ValueError) as e: - _ = multilabel_classfication.get_label_quality_scores( + _ = ml_classification.get_label_quality_scores( formatted_labels, pred_probs, method="badchoice" ) assert "Invalid method name: badchoice" in str(e.value) with pytest.raises(ValueError) as e: - _ = multilabel_classfication.get_label_quality_scores( + _ = ml_classification.get_label_quality_scores( formatted_labels, pred_probs, aggregator_kwargs={"method": "invalid"} ) assert "Invalid aggregation method specified: 'invalid'" in str(e.value) @@ -172,7 +215,7 @@ def base_scores(self): ids=lambda x: x.__name__ if callable(x) else str(x), ) def test_aggregator_callable(self, method): - aggregator = multilabel_classfication.Aggregator(method=method) + aggregator = ml_scorer.Aggregator(method=method) assert callable(aggregator.method), "Aggregator should store a callable method" assert callable(aggregator), "Aggregator should be callable" @@ -189,24 +232,24 @@ def test_aggregator_callable(self, method): ids=["min", "max", "mean", "median", "exponential_moving_average", "softmin"], ) def test_aggregator_score(self, base_scores, method, expected_score): - aggregator = multilabel_classfication.Aggregator(method=method) + aggregator = ml_scorer.Aggregator(method=method) scores = aggregator(base_scores) assert np.isclose(scores, np.array([expected_score]), rtol=1e-3).all() assert scores.shape == (1,) def test_invalid_method(self): with pytest.raises(ValueError) as e: - _ = multilabel_classfication.Aggregator(method="invalid_method") + _ = ml_scorer.Aggregator(method="invalid_method") assert "Invalid aggregation method specified: 'invalid_method'" in str( e.value ), "String constructor has limited options" with pytest.raises(TypeError) as e: - _ = multilabel_classfication.Aggregator(method=1) + _ = ml_scorer.Aggregator(method=1) assert "Expected callable method" in str(e.value), "Non-callable methods are not valid" def test_invalid_score(self, base_scores): - aggregator = multilabel_classfication.Aggregator(method=np.min) + aggregator = ml_scorer.Aggregator(method=np.min) with pytest.raises(ValueError) as e: _ = aggregator(base_scores[0]) assert "Expected 2D array" in str(e.value), "Aggregator expects 2D array" @@ -304,6 +347,152 @@ def test_is_multilabel(labels): assert not ml_scorer._is_multilabel(labels[:, 0]) +@pytest.mark.parametrize("class_names", [None, ["Apple", "Cat", "Dog", "Peach", "Bird"]]) +def test_common_multilabel_issues(class_names, pred_probs_multilabel, labels_multilabel): + df = common_multilabel_issues( + labels=labels_multilabel, pred_probs=pred_probs_multilabel, class_names=class_names + ) + expected_issue_probabilities = [ + 0.14285714285714285, + 0.14285714285714285, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ] + assert len(df) == 10 + assert np.isclose(np.array(expected_issue_probabilities), df["Issue Probability"]).all() + if class_names: + expected_res = [ + "Apple", + "Dog", + "Apple", + "Cat", + "Cat", + "Dog", + "Peach", + "Peach", + "Bird", + "Bird", + ] + assert list(df["Class Name"]) == expected_res + else: + assert "Class Name" not in df.columns + + +@pytest.mark.parametrize("min_examples_per_class", [10, 90]) +def test_multilabel_min_examples_per_class(data_multilabel, min_examples_per_class): + labels, pred_probs = data_multilabel + issues = filter.find_label_issues( + labels=labels, pred_probs=pred_probs, min_examples_per_class=min_examples_per_class + ) + if min_examples_per_class == 10: + assert sum(issues) == 9 + else: + assert sum(issues) == 0 + + +@pytest.mark.parametrize("num_to_remove_per_class", [None, [1, 1, 0, 0, 2], [1, 1, 0, 0, 1]]) +def test_multilabel_num_to_remove_per_class(data_multilabel, num_to_remove_per_class): + labels, pred_probs = data_multilabel + + issues = filter.find_label_issues( + labels=labels, pred_probs=pred_probs, num_to_remove_per_class=num_to_remove_per_class + ) + num_issues = sum(issues) + if num_to_remove_per_class is None: + assert num_issues == 9 + else: + assert num_issues == sum(num_to_remove_per_class) + + +@pytest.mark.parametrize("class_names", [None, ["Apple", "Cat", "Dog", "Peach", "Bird"]]) +def test_rank_classes_by_multilabel_quality(pred_probs_multilabel, labels_multilabel, class_names): + df_ranked = rank_classes_by_multilabel_quality( + pred_probs=pred_probs_multilabel, labels=labels_multilabel, class_names=class_names + ) + expected_Label_Issues = [1, 0, 0, 0, 0] + + expected_Label_Noise = [0.14285714285714285, 0.0, 0.0, 0.0, 0.0] + + expected_Label_Quality_Score = [0.8571428571428572, 1.0, 1.0, 1.0, 1.0] + + expected_Inverse_Label_Issues = [0, 1, 0, 0, 0] + + expected_Inverse_Label_Noise = [0.0, 0.14285714285714285, 0.0, 0.0, 0.0] + assert list(df_ranked["Label Issues"]) == expected_Label_Issues + + assert np.isclose(np.array(expected_Label_Noise), df_ranked["Label Noise"]).all() + assert np.isclose( + np.array(expected_Label_Quality_Score), df_ranked["Label Quality Score"] + ).all() + assert list(df_ranked["Inverse Label Issues"]) == expected_Inverse_Label_Issues + assert np.isclose( + np.array(expected_Inverse_Label_Noise), df_ranked["Inverse Label Noise"] + ).all() + if class_names: + expected_res = [ + "Dog", + "Apple", + "Cat", + "Peach", + "Bird", + ] + assert list(df_ranked["Class Name"]) == expected_res + else: + assert "Class Name" not in df_ranked.columns + + +def test_overall_multilabel_health_score(data_multilabel): + labels, pred_probs = data_multilabel + overall_label_health_score = overall_multilabel_health_score( + pred_probs=pred_probs, labels=labels + ) + assert np.isclose(overall_label_health_score, 0.91) + + +def test_get_class_label_quality_scores(): + pred_probs = np.array( + [ + [0.9, 0.1, 0.0, 0.4, 0.1], + [0.7, 0.8, 0.2, 0.3, 0.1], + [0.9, 0.8, 0.4, 0.2, 0.1], + [0.1, 0.1, 0.8, 0.3, 0.1], + [0.4, 0.5, 0.1, 0.1, 0.1], + [0.1, 0.1, 0.2, 0.1, 0.1], + [0.8, 0.1, 0.2, 0.1, 0.1], + ] + ) + labels = [[0], [0, 1], [0, 1], [2], [0, 2, 3], [], []] + scores = get_label_quality_scores_per_class(pred_probs=pred_probs, labels=labels) + expected_res = [ + [0.9, 0.9, 1.0, 0.6, 0.9], + [0.7, 0.8, 0.8, 0.7, 0.9], + [0.9, 0.8, 0.6, 0.8, 0.9], + [0.9, 0.9, 0.8, 0.7, 0.9], + [0.4, 0.5, 0.1, 0.1, 0.9], + [0.9, 0.9, 0.8, 0.9, 0.9], + [0.2, 0.9, 0.8, 0.9, 0.9], + ] + assert np.isclose(scores, np.array(expected_res)).all() + + +def test_health_summary_multilabel(pred_probs_multilabel, labels_multilabel): + health_summary_multilabel = multilabel_health_summary( + pred_probs=pred_probs_multilabel, labels=labels_multilabel + ) + expected_keys = [ + "classes_by_multilabel_quality", + "common_multilabel_issues", + "overall_multilabel_health_score", + ] + assert sorted(health_summary_multilabel.keys()) == expected_keys + + @pytest.mark.parametrize( "input", [ @@ -388,7 +577,6 @@ def test_multilabel_py(given_labels, expected): @pytest.mark.parametrize("K", [2, 3, 4], ids=["K=2", "K=3", "K=4"]) def test_get_split_generator(cv, K): - all_configurations = np.array(list(itertools.product([0, 1], repeat=K))) given_labels = np.repeat(all_configurations, 2, axis=0) @@ -412,7 +600,6 @@ def test_get_split_generator(cv, K): # Test split_generator with rare/missing multilabel configurations @pytest.mark.parametrize("K", [2, 3, 4], ids=["K=2", "K=3", "K=4"]) def test_get_split_generator_rare_configurations(cv, K): - all_configurations = np.array(list(itertools.product([0, 1], repeat=K))) given_labels = np.repeat(all_configurations, 2, axis=0) diff --git a/tests/test_object_detection.py b/tests/test_object_detection.py new file mode 100644 index 0000000000..d9ca5f770a --- /dev/null +++ b/tests/test_object_detection.py @@ -0,0 +1,603 @@ +import os + +from cleanlab.internal.object_detection_utils import ( + softmin1d, + softmax, + bbox_xyxy_to_xywh, +) + +from cleanlab.object_detection.rank import ( + get_label_quality_scores, + issues_from_scores, + _get_min_pred_prob, + _get_valid_score, + _prune_by_threshold, + _compute_label_quality_scores, + _separate_label, + _separate_prediction, + _get_overlap_matrix, + _get_dist_matrix, + _get_valid_inputs_for_compute_scores, + _get_valid_inputs_for_compute_scores_per_image, + compute_overlooked_box_scores, + compute_badloc_box_scores, + compute_swap_box_scores, + _get_prediction_type, + _get_valid_subtype_score_params, + _get_aggregation_weights, +) + +from cleanlab.object_detection.filter import ( + find_label_issues, + _find_label_issues_per_box, + _pool_box_scores_per_image, + _find_label_issues, +) + +from cleanlab.object_detection.summary import ( + visualize, +) +from cleanlab.internal.constants import ( + ALPHA, + LOW_PROBABILITY_THRESHOLD, + HIGH_PROBABILITY_THRESHOLD, + OVERLOOKED_THRESHOLD, + BADLOC_THRESHOLD, + SWAP_THRESHOLD, + TEMPERATURE, + CUSTOM_SCORE_WEIGHT_OVERLOOKED, + CUSTOM_SCORE_WEIGHT_SWAP, + CUSTOM_SCORE_WEIGHT_BADLOC, +) + +import numpy as np + +np.random.seed(0) + +import warnings + +import pytest + +from PIL import Image +import numpy as np +import copy + +# to suppress plt.show() +import matplotlib.pyplot as plt + + +def generate_image(arr=None): + """Generates single image of randomly colored pixels""" + if arr is None: + arr = np.random.randint(low=0, high=256, size=(300, 300, 3), dtype=np.uint8) + img = Image.fromarray(arr, mode="RGB") + return img + + +@pytest.fixture(scope="session") +def generate_single_image_file(tmpdir_factory, img_name="img.png", arr=None): + """Generates a single temporary image for testing""" + img = generate_image(arr) + fn = tmpdir_factory.mktemp("data").join(img_name) + img.save(str(fn)) + return str(fn) + + +@pytest.fixture(scope="session") +def generate_n_image_files(tmpdir_factory, n=5): + """Generates n temporary images for testing and returns dir of images""" + filename_list = [] + tmp_image_dir = tmpdir_factory.mktemp("data") + for i in range(n): + img = generate_image() + img_name = f"{i}.png" + fn = tmp_image_dir.join(img_name) + img.save(str(fn)) + filename_list.append(str(fn)) + return str(tmp_image_dir) + + +def generate_predictions( + num_predictions, annotations, num_classes=5, max_boxes=6, image_size=300, is_issue=False +): + """Generates num_predictions number of predictions based on passed in hyperparameters in same format as expected by find_label_issues and get_label_quality_scores""" + + predictions = [] + if isinstance(is_issue, int): + is_issue = [is_issue] * num_predictions + for i in range(num_predictions): + issue = is_issue[i] + annotation = annotations[i] if i < len(annotations) else None + prediction = generate_prediction(annotation, num_classes, image_size, max_boxes, issue) + if prediction is not None: + predictions.append(prediction) + return predictions + + +def generate_prediction(annotation, num_classes, image_size, max_boxes, issue): + """Generates a single prediction based on passed in hyperparameters in same format as expected by find_label_issues and get_label_quality_scores""" + + prediction = [[] for _ in range(num_classes)] + if annotation is None and issue is False: + return + else: + if issue is False: + for label, bboox in zip(annotation["labels"], annotation["bboxes"]): + rand_probability = np.random.randint(low=96, high=100) / 100 + prediction[label].append(list(bboox) + [rand_probability]) + else: + num_predictions = np.random.randint(low=1, high=max_boxes + 1) + rand_labels = generate_labels(num_classes, num_predictions) + for label in rand_labels: + rand_bbox = generate_bbox(image_size) + rand_probability = np.random.randint(low=96, high=100) / 100 + prediction[label].append(list(rand_bbox) + [rand_probability]) + prediction = [ + np.array(p) if len(p) > 0 else np.empty(shape=[0, 5], dtype=np.float32) + for p in prediction + ] + return np.array(prediction, dtype=object) + + +def generate_annotations(num_annotations, num_classes=5, max_boxes=5, image_size=300): + """Generates num_annotations number of annotations based on passed in hyperparameters in same format as expected by find_label_issues and get_label_quality_scores""" + + annotations = [] + for i in range(num_annotations): + annotations.append(generate_annotation(num_classes, image_size, max_boxes)) + return annotations + + +def generate_annotation(num_classes, image_size, max_boxes): + """Generates a single annotation based on passed in hyperparameters in same format as expected by find_label_issues and get_label_quality_scores""" + + num_boxes = np.random.randint(low=1, high=max_boxes) + bboxes = np.array([generate_bbox(image_size) for _ in range(num_boxes)]) + labels = generate_labels(num_classes, num_boxes) + annotation = {"bboxes": bboxes, "labels": labels} + return annotation + + +def generate_labels(num_classes, num_boxes): + """Generates num_boxes number of labels with possible values [0-num_classes)""" + return np.random.choice(num_classes, num_boxes) + + +def generate_bbox(image_size): + """Generates a single bounding box x1,y1,x2,y2 with coordinates lower than image_size""" + x2 = np.random.randint(low=2, high=image_size - 1) + y2 = np.random.randint(low=2, high=image_size - 1) + x_shift = np.random.randint(low=1, high=x2) + y_shift = np.random.randint(low=1, high=y2) + x1 = x2 - x_shift + y1 = y2 - y_shift + return [x1, y1, x2, y2] + + +warnings.filterwarnings("ignore") + +good_labels = generate_annotations(5, num_classes=10, max_boxes=10) +good_predictions = generate_predictions( + 5, good_labels, num_classes=10, max_boxes=12, is_issue=False +) + +bad_labels = generate_annotations(5, num_classes=10, max_boxes=10) +bad_predictions = generate_predictions(5, bad_labels, num_classes=10, max_boxes=12, is_issue=True) + +labels = good_labels + bad_labels # 10 labels +predictions = ( + good_predictions + bad_predictions +) # 10 predictions, [:5] is perfect predictions, [5:] is bad predictions + + +def test_get_label_quality_scores(): + scores = get_label_quality_scores(labels, predictions) + assert len(scores) == len(labels) + assert (scores <= 1.0).all() + assert len(scores.shape) == 1 + assert (scores[:5] > 0.9).all() # perfect annotations get high scores + assert (scores[5:] < 0.7).all() # label issues get low scores + + +@pytest.mark.parametrize( + "agg_weights", + [ + {"overlooked": 1.0, "swap": 0.0, "badloc": 0.0}, + {"overlooked": 0.0, "swap": 1.0, "badloc": 0.0}, + {"overlooked": 0.0, "swap": 0.0, "badloc": 1.0}, + ], +) +def test_get_label_quality_scores_custom_weights(agg_weights): + scores = get_label_quality_scores(labels, predictions, aggregation_weights=agg_weights) + assert (scores[:5] > 0.8).all() # perfect annotations get high scores + + if agg_weights["swap"] == 1.0: + assert (scores[5:][scores[5:] != 1.0] < 0.8).any() # swapped label issues get low scores + elif agg_weights["overlooked"] == 1.0: + assert (scores[5:][scores[5:] != 1.0] < 0.7).all() # overlooked label issues get low scores + elif agg_weights["badloc"] == 1.0: + assert (scores[5:][scores[5:] != 1.0] < 0.7).all() # label issues get low scores + + +def test_issues_from_scores(): + scores = get_label_quality_scores(labels, predictions) + real_issue_from_scores = issues_from_scores(scores, threshold=1.0) + assert len(real_issue_from_scores) == len(scores) + assert np.argmin(scores) == real_issue_from_scores[0] + + fake_scores = np.array([0.2, 0.4, 0.6, 0.1]) + fake_threshold = 0.3 + fake_issue_from_scores = issues_from_scores(fake_scores, threshold=fake_threshold) + assert (fake_issue_from_scores == np.array([3, 0])).all() + + +def test_get_min_pred_prob(): + min = _get_min_pred_prob(predictions) + assert min == 0.96 + + +def test_get_valid_score(): + score = _get_valid_score(np.array([]), temperature=0.99) + assert score == 1.0 + + score_larger = _get_valid_score(np.array([0.8, 0.7, 0.6]), temperature=0.99) + score_smaller = _get_valid_score(np.array([0.8, 0.7, 0.6]), temperature=0.2) + assert score_smaller < score_larger + + +def test_get_valid_subtype_score_params(): + ( + alpha, + low_probability_threshold, + high_probability_threshold, + temperature, + ) = _get_valid_subtype_score_params(None, None, None, None) + assert alpha == ALPHA + assert low_probability_threshold == LOW_PROBABILITY_THRESHOLD + assert high_probability_threshold == HIGH_PROBABILITY_THRESHOLD + assert temperature == TEMPERATURE + + +def test_get_aggregation_weights(): + correct_aggregation_weights = { + "overlooked": CUSTOM_SCORE_WEIGHT_OVERLOOKED, + "swap": CUSTOM_SCORE_WEIGHT_SWAP, + "badloc": CUSTOM_SCORE_WEIGHT_BADLOC, + } + weights = _get_aggregation_weights(None) + assert weights == correct_aggregation_weights + + with pytest.raises(ValueError) as e: + _get_aggregation_weights( + { + "overlooked": -1.0, + "swap": CUSTOM_SCORE_WEIGHT_SWAP, + "badloc": CUSTOM_SCORE_WEIGHT_BADLOC, + } + ) + + with pytest.raises(ValueError) as e: + _get_aggregation_weights( + { + "overlooked": CUSTOM_SCORE_WEIGHT_OVERLOOKED, + "swap": 1.2, + "badloc": CUSTOM_SCORE_WEIGHT_BADLOC, + } + ) + + +def test_softmin1d(): + small_val = 0.004 + assert softmin1d([small_val]) == small_val + + +def test_softmax(): + small_val = 0.004 + assert softmax(np.array([small_val])) == 1.0 + + +def test_bbox_xyxy_to_xywh(): + box_coords = bbox_xyxy_to_xywh([5, 4, 2, 5, 0.86]) + assert box_coords is None + box_coords = bbox_xyxy_to_xywh([5, 4, 2, 5]) + assert box_coords is not None + + +@pytest.mark.filterwarnings("ignore::UserWarning") # Should be 2 warnings (first two calls) +def test_prune_by_threshold(): + pruned_predictions = _prune_by_threshold(predictions, 1.0) + print(pruned_predictions) + for image_pred in pruned_predictions: + for class_pred in image_pred: + assert class_pred.shape[0] == 0 + + pruned_predictions = _prune_by_threshold(predictions, 0.6) + + num_boxes_not_pruned = 0 + for image_pred in pruned_predictions: + for class_pred in image_pred: + if class_pred.shape[0] > 0: + num_boxes_not_pruned += 1 + assert num_boxes_not_pruned == 44 + + pruned_predictions = _prune_by_threshold(predictions, 0.5) + for im0, im1 in zip(pruned_predictions, predictions): + for cl0, cl1 in zip(im0, im1): + assert (cl0 == cl1).all() + + +def test_similarity_matrix(): + ALPHA = 0.99 + lab_bboxes, lab_labels = _separate_label(labels[0]) + det_bboxes, det_labels, det_label_prob = _separate_prediction(predictions[0]) + + iou_matrix = _get_overlap_matrix(lab_bboxes, det_bboxes) + dist_matrix = 1 - _get_dist_matrix(lab_bboxes, det_bboxes) + + similarity_matrix = iou_matrix * ALPHA + (1 - ALPHA) * (1 - dist_matrix) + assert (similarity_matrix.flatten() >= 0).all() and (similarity_matrix.flatten() <= 1).all() + + +def test_compute_label_quality_scores(): + scores = _compute_label_quality_scores(labels, predictions) + scores_with_threshold = _compute_label_quality_scores(labels, predictions, threshold=0.99) + assert np.sum(scores) != np.sum(scores_with_threshold) + + min_pred_prob = _get_min_pred_prob(predictions) + scores_with_min_threshold = _compute_label_quality_scores( + labels, predictions, threshold=min_pred_prob + ) + assert (scores == scores_with_min_threshold).all() + + +def test_overlooked_score_shifts_in_correct_direction(): + perfect_label = labels[0] + bad_label = copy.deepcopy(labels[0]) + worst_label = copy.deepcopy(labels[0]) + + print(predictions[0]) + print(bad_label) + bad_label["bboxes"] = np.delete(bad_label["bboxes"], 2, axis=0) # 0.79 pred_probs + worst_label["bboxes"] = np.delete(worst_label["bboxes"], -1, axis=0) # 0.84 pred_probs + + bad_label["labels"] = np.delete(bad_label["labels"], 2) + worst_label["labels"] = np.delete(worst_label["labels"], -1) + + scores = _compute_label_quality_scores( + [perfect_label, bad_label, worst_label], [predictions[0], predictions[0], predictions[0]] + ) + + assert scores[0] > scores[1] + assert scores[1] > scores[2] + + +def test_badloc_score_shifts_in_correct_direction(): + perfect_label = labels[0] + bad_label = copy.deepcopy(labels[0]) + worst_label = copy.deepcopy(labels[0]) + + bad_label["bboxes"][0] = bad_label["bboxes"][0] - 20 + worst_label["bboxes"][0] = worst_label["bboxes"][0] - 100 + + scores = _compute_label_quality_scores( + [perfect_label, bad_label, worst_label], [predictions[0], predictions[0], predictions[0]] + ) + assert scores[0] > scores[1] + assert scores[1] > scores[2] + + +def test_swap_score_shifts_in_correct_direction(): + perfect_label = labels[0] + bad_label = copy.deepcopy(labels[0]) + worst_label = copy.deepcopy(labels[0]) + + bad_label["bboxes"][0] = bad_label["bboxes"][0] - 20 + bad_label["labels"][0] = np.random.choice([i for i in range(10) if i != bad_label["labels"][0]]) + worst_label["bboxes"][0] = worst_label["bboxes"][0] - 100 + worst_label["labels"][0] = np.random.choice( + [i for i in range(10) if i != bad_label["labels"][0]] + ) + + scores = _compute_label_quality_scores( + [perfect_label, bad_label, worst_label], [predictions[0], predictions[0], predictions[0]] + ) + assert scores[0] > scores[1] + assert scores[1] > scores[2] + + +def test_find_label_issues(): + auxiliary_inputs = _get_valid_inputs_for_compute_scores(ALPHA, labels, predictions) + test_inputs = _get_valid_inputs_for_compute_scores_per_image( + alpha=ALPHA, label=labels[0], prediction=predictions[0] + ) + + assert (test_inputs["pred_label_probs"] == auxiliary_inputs[0]["pred_label_probs"]).all() + + overlooked_scores_per_box = compute_overlooked_box_scores( + alpha=ALPHA, + high_probability_threshold=HIGH_PROBABILITY_THRESHOLD, + auxiliary_inputs=auxiliary_inputs, + ) + + overlooked_scores_no_auxillary_inputs = compute_overlooked_box_scores( + alpha=ALPHA, + high_probability_threshold=HIGH_PROBABILITY_THRESHOLD, + labels=labels, + predictions=predictions, + ) + + for score, no_auxiliary_inputs_score in zip( + overlooked_scores_per_box, overlooked_scores_no_auxillary_inputs + ): + assert ( + score[~np.isnan(score)] + == no_auxiliary_inputs_score[~np.isnan(no_auxiliary_inputs_score)] + ).all() + + overlooked_issues_per_box = _find_label_issues_per_box( + overlooked_scores_per_box, OVERLOOKED_THRESHOLD + ) + overlooked_issues_per_image = _pool_box_scores_per_image(overlooked_issues_per_box) + overlooked_issues = np.sum(overlooked_issues_per_image) + assert np.sum(overlooked_issues_per_image[5:]) == 5 # check bad labels were detected correctly + assert overlooked_issues == 5 + + badloc_scores_per_box = compute_badloc_box_scores( + alpha=ALPHA, + low_probability_threshold=LOW_PROBABILITY_THRESHOLD, + auxiliary_inputs=auxiliary_inputs, + ) + + badloc_scores_no_auxillary_inputs = compute_badloc_box_scores( + alpha=ALPHA, + low_probability_threshold=LOW_PROBABILITY_THRESHOLD, + labels=labels, + predictions=predictions, + ) + + for score, no_auxiliary_inputs_score in zip( + badloc_scores_per_box, badloc_scores_no_auxillary_inputs + ): + assert (score == no_auxiliary_inputs_score).all() + + badloc_issues_per_box = _find_label_issues_per_box(badloc_scores_per_box, BADLOC_THRESHOLD) + badloc_issues_per_image = _pool_box_scores_per_image(badloc_issues_per_box) + badloc_issues = np.sum(badloc_issues_per_image) + assert np.sum(badloc_issues_per_image[5:]) == 4 # check bad labels were detected correctly + assert badloc_issues == 4 + + swap_scores_per_box = compute_swap_box_scores( + alpha=ALPHA, + high_probability_threshold=HIGH_PROBABILITY_THRESHOLD, + auxiliary_inputs=auxiliary_inputs, + ) + + swap_scores_no_auxillary_inputs = compute_swap_box_scores( + alpha=ALPHA, + high_probability_threshold=HIGH_PROBABILITY_THRESHOLD, + labels=labels, + predictions=predictions, + ) + + for score, no_auxiliary_inputs_score in zip( + swap_scores_per_box, swap_scores_no_auxillary_inputs + ): + assert (score == no_auxiliary_inputs_score).all() + + swap_issues_per_box = _find_label_issues_per_box(swap_scores_per_box, SWAP_THRESHOLD) + swap_issues_per_image = _pool_box_scores_per_image(swap_issues_per_box) + swap_issues = np.sum(swap_issues_per_image) + assert np.sum(swap_scores_per_box[2]) > np.sum(swap_scores_per_box[7]) + assert swap_issues == 0 + + label_issues = find_label_issues(labels, predictions) + assert np.sum(label_issues) == np.sum( + (swap_issues_per_image + badloc_issues_per_image + overlooked_issues_per_image) > 0 + ) + assert np.sum(label_issues[5:]) == 5 # check bad labels were detected correctly + + swap_issues_per_box = _find_label_issues_per_box(swap_scores_per_box, 0.7) + swap_issues_per_image = _pool_box_scores_per_image(swap_issues_per_box) + swap_issues = np.sum(swap_issues_per_image) + assert swap_issues == 1 + assert np.sum(swap_issues_per_image[5:]) == 1 # check bad labels were detected correctly + + +def test_separate_prediction(): + pred_bboxes = np.array( + [ + np.array(list(generate_bbox(300)) + [0.97]), + np.empty(shape=[0, 5], dtype=np.float32), + np.array(list(generate_bbox(300)) + [0.94]), + ], + dtype=object, + ) + pred_labels = np.array([0, 2]) + pred_probs = np.array([[0.98, 0.01, 0.01], [0.02, 0.02, 0.98]]) + all_pred_prediction = np.array([pred_bboxes, pred_labels, pred_probs], dtype=object) + prediction_type = _get_prediction_type(all_pred_prediction) + assert prediction_type == "all_pred" + + boxes, labels, pred_probs = _separate_prediction( + all_pred_prediction, prediction_type=prediction_type + ) + assert len(labels) == len(pred_probs) + + +def test_return_issues_ranked_by_scores(): + label_issue_idx = find_label_issues(labels, predictions, return_indices_ranked_by_score=True) + assert ( + len(set([5, 6, 7, 8, 9]).intersection(label_issue_idx[:5])) == 5 + ) # lower scores for bad examples + assert len(label_issue_idx) == 5 # no good example index returned + + +def test_bad_input_find_label_issues_internal(): + bad_label_issues = _find_label_issues(labels, predictions, scoring_method="bad_method") + assert (bad_label_issues == -1).all() + + +def test_find_label_issues_per_box(): + scores_per_box = [np.array([0.2, 0.3]), np.array([]), np.array([0.9, 0.5, 0.9, 0.51])] + issues_per_box = _find_label_issues_per_box(scores_per_box, threshold=0.5) + assert issues_per_box[1] == np.array([False]) + assert (issues_per_box[0] == np.array([True, True])).all() + assert (issues_per_box[2] == np.array([False, True, False, False])).all() + + +@pytest.mark.usefixtures("generate_single_image_file") +def test_visualize(monkeypatch, generate_single_image_file): + monkeypatch.setattr(plt, "show", lambda: None) + + arr = np.random.randint(low=0, high=256, size=(300, 300, 3), dtype=np.uint8) + img = Image.fromarray(arr, mode="RGB") + visualize(img) + + visualize(img, save_path="./fake_path.pdf") + assert os.path.exists("./fake_path.pdf") + + visualize(img, save_path="./fake_path_no_ext") + assert os.path.exists("./fake_path_no_ext.png") + + visualize(img, save_path="./fake_path.ps") + assert os.path.exists("./fake_path.ps") + + visualize(img, save_path="./fake.path.pdf") + assert os.path.exists("./fake.path.pdf") + + visualize(generate_single_image_file, label=labels[0], prediction=predictions[0]) + visualize(generate_single_image_file, label=None, prediction=predictions[0]) + visualize(generate_single_image_file, label=labels[0], prediction=None) + visualize(generate_single_image_file, label=None, prediction=None) + + visualize(generate_single_image_file, label=None, prediction=predictions[0], overlay=False) + visualize(generate_single_image_file, label=labels[0], prediction=None, overlay=False) + visualize(generate_single_image_file, label=None, prediction=None, overlay=False) + + visualize( + generate_single_image_file, + label=labels[0], + prediction=predictions[0], + prediction_threshold=0.99, + overlay=False, + ) + + visualize( + generate_single_image_file, + label=labels[0], + prediction=predictions[0], + prediction_threshold=0.99, + class_names={ + "0": "car", + "1": "chair", + "2": "cup", + "3": "person", + "4": "traffic light", + "5": "5", + "6": "6", + "7": "7", + "8": "8", + "9": "9", + }, + overlay=False, + ) diff --git a/tests/test_outlier.py b/tests/test_outlier.py index 545b008de5..f1f5b2bfd6 100644 --- a/tests/test_outlier.py +++ b/tests/test_outlier.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -251,11 +251,12 @@ def test_class_public_func(): # Testing regular fit OOD_ood = OutOfDistribution() + print(OOD_ood.params) OOD_ood.fit(pred_probs=pred_probs, labels=labels) - + print(OOD_ood.params) OOD_outlier = OutOfDistribution() OOD_outlier.fit(features=features) - + print(OOD_outlier.params) assert OOD_ood.params["confident_thresholds"] is not None and OOD_ood.params["knn"] is None assert ( OOD_outlier.params["knn"] is not None and OOD_outlier.params["confident_thresholds"] is None @@ -270,6 +271,26 @@ def test_class_public_func(): OOD_ood_already_fit.params["confident_thresholds"] == confident_thresholds ).all() # Assert not overwritten + # Testing fit uses correct metrics given feature dimensionality + X_small = np.random.rand(20, 3) + OOD_euclidean = OutOfDistribution() + OOD_euclidean.fit(features=X_small) + assert OOD_euclidean.params["knn"].metric == "euclidean" + X_small_with_ood = np.vstack([X_small, [999999.0] * 3]) + euclidean_score = OOD_euclidean.score(features=X_small_with_ood) + assert (np.max(euclidean_score) <= 1) and (np.min(euclidean_score) >= 0) + assert np.argmin(euclidean_score) == (euclidean_score.shape[0] - 1) + + # Re-run tests with high dimensional dataset + X_large = np.hstack([np.zeros((200, 400)), np.random.rand(200, 1)]) + OOD_cosine = OutOfDistribution() + OOD_cosine.fit(features=X_large) + assert OOD_cosine.params["knn"].metric == "cosine" + X_large_with_ood = np.vstack([X_large, [999999.0] * 401]) + cosine_score = OOD_cosine.score(features=X_large_with_ood) + assert (np.max(cosine_score) <= 1) and (np.min(cosine_score) >= 0) + assert np.argmin(cosine_score) == (cosine_score.shape[0] - 1) + #### TESTING SCORE ood_score = OOD_ood.score(pred_probs=pred_probs) outlier_score = OOD_outlier.score(features=features) @@ -329,14 +350,12 @@ def test_get_ood_features_scores(): X_test_with_ood = np.vstack([X_test, X_ood]) # Fit nearest neighbors on X_train - knn = NearestNeighbors(n_neighbors=5).fit(X_train) - + knn = NearestNeighbors(n_neighbors=5, metric="euclidean").fit(X_train) # Get KNN distance as outlier score k = 5 knn_distance_to_score, _ = outlier._get_ood_features_scores( features=X_test_with_ood, knn=knn, k=k ) - # Checking that X_ood has the smallest outlier score among all the datapoints assert np.argmin(knn_distance_to_score) == (knn_distance_to_score.shape[0] - 1) @@ -376,7 +395,7 @@ def test_default_k_and_model_get_ood_features_scores(): instantiated_k = 10 # Create NN class object with small instantiated k and fit on data - knn = NearestNeighbors(n_neighbors=instantiated_k, metric="cosine").fit(X_with_ood) + knn = NearestNeighbors(n_neighbors=instantiated_k, metric="euclidean").fit(X_with_ood) avg_knn_distances_default_model, _ = outlier._get_ood_features_scores( features=X_with_ood, diff --git a/tests/test_rank.py b/tests/test_rank.py index 53344217c9..e928587774 100644 --- a/tests/test_rank.py +++ b/tests/test_rank.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2022 Cleanlab Inc. +# Copyright (C) 2017-2023 Cleanlab Inc. # This file is part of cleanlab. # # cleanlab is free software: you can redistribute it and/or modify @@ -153,7 +153,6 @@ def test_order_label_issues_using_scoring_func_ranking(scoring_method_func, adju # do not run the test below if the method does not support adjust_pred_probs # confidence_weighted_entropy scoring method does not support adjust_pred_probs if not (adjust_pred_probs == True and method == "confidence_weighted_entropy"): - indices = np.arange(len(data["label_errors_mask"]))[ data["label_errors_mask"] ] # indices of label issues diff --git a/tests/test_regression.py b/tests/test_regression.py new file mode 100644 index 0000000000..f02750ee6b --- /dev/null +++ b/tests/test_regression.py @@ -0,0 +1,245 @@ +import pytest +import random +import numpy as np +import pandas as pd +from sklearn.svm import SVR +from sklearn.metrics import r2_score +from sklearn.linear_model import LinearRegression +from cleanlab.regression.rank import ( + get_label_quality_scores, + _get_residual_score_for_each_label, + _get_outre_score_for_each_label, +) +from cleanlab.regression.learn import CleanLearning + +# set seed for reproducability +SEED = 1 +np.random.seed(SEED) +random.seed(SEED) + + +def make_data(num_examples=200, num_features=3, noise=0.2, error_frac=0.1, error_noise=5): + X = np.random.random(size=(num_examples, num_features)) + coefficients = np.random.uniform(-1, 1, size=num_features) + label_noise = np.random.normal(scale=noise, size=num_examples) + + true_y = np.dot(X, coefficients) + y = np.dot(X, coefficients) + label_noise + + # add extra noisy examples + num_errors = int(num_examples * error_frac) + extra_noise = np.random.normal(scale=error_noise, size=num_errors) + random_idx = np.random.choice(num_examples, num_errors) + y[random_idx] += extra_noise + error_idx = np.argsort(abs(y - true_y))[-num_errors:] # get the noisiest examples idx + + # create test set + X_test = np.random.random(size=(num_examples, num_features)) + label_noise = np.random.normal(scale=noise, size=num_examples) + y_test = np.dot(X_test, coefficients) + label_noise + + return { + "X": X, + "y": y, + "true_y": true_y, + "X_test": X_test, + "y_test": y_test, + "error_idx": error_idx, + } + + +# To be used for most tests +data = make_data() +X, labels, predictions = data["X"], data["y"], data["true_y"] +error_idx = data["error_idx"] +X_test, y_test = data["X_test"], data["y_test"] +y = labels # for ease + +# Used for characterization tests +small_labels = np.array([1, 2, 3, 4]) +small_predictions = np.array([2, 2, 5, 4.1]) +expected_score_outre = np.array([0.04536998, 0.38809391, 0.03983538, 0.38809391]) +expected_score_residual = np.array([0.36787944, 1.0, 0.13533528, 0.90483742]) +expected_scores = {"outre": expected_score_outre, "residual": expected_score_residual} + +# Inputs that are not array like +aConstant = 1 +aString = "predictions_non_array" +aDict = {"labels": [1, 2], "predictions": [2, 3]} +aSet = {1, 2, 3, 4} +aBool = True + + +@pytest.fixture +def non_array_input(): + return [aConstant, aString, aDict, aSet, aBool] + + +# test with deafault parameters +def test_output_shape_type(): + scores = get_label_quality_scores(labels=labels, predictions=predictions) + assert labels.shape == scores.shape + assert isinstance(scores, np.ndarray) + + +def test_labels_are_arraylike(non_array_input): + for new_input in non_array_input: + with pytest.raises(ValueError) as error: + get_label_quality_scores(labels=new_input, predictions=predictions) + assert error.type == ValueError + + +def test_predictionns_are_arraylike(non_array_input): + for new_input in non_array_input: + with pytest.raises(ValueError) as error: + get_label_quality_scores(labels=labels, predictions=new_input) + assert error.type == ValueError + + +# test for input shapes +def test_input_shape_labels(): + with pytest.raises(AssertionError) as error: + get_label_quality_scores(labels=labels[:-1], predictions=predictions) + assert ( + str(error.value) + == f"Number of examples in labels {labels[:-1].shape} and predictions {predictions.shape} are not same." + ) + + +def test_input_shape_predictions(): + with pytest.raises(AssertionError) as error: + get_label_quality_scores(labels=labels, predictions=predictions[:-1]) + assert ( + str(error.value) + == f"Number of examples in labels {labels.shape} and predictions {predictions[:-1].shape} are not same." + ) + + +# test individual scoring functions +@pytest.mark.parametrize( + "scoring_funcs", + [_get_residual_score_for_each_label, _get_outre_score_for_each_label], +) +def test_individual_scoring_functions(scoring_funcs): + scores = scoring_funcs(labels=labels, predictions=predictions) + assert labels.shape == scores.shape + assert isinstance(scores, np.ndarray) + + +# test for method argument +@pytest.mark.parametrize( + "method", + [ + "residual", + "outre", + ], +) +def test_method_pass_get_label_quality_scores(method): + scores = get_label_quality_scores(labels=labels, predictions=predictions, method=method) + assert labels.shape == scores.shape + assert isinstance(scores, np.ndarray) + + +@pytest.mark.parametrize( + "method", + [ + "residual", + "outre", + ], +) +def test_expected_scores(method): + # characterization test + scores = get_label_quality_scores( + labels=small_labels, predictions=small_predictions, method=method + ) + assert np.allclose(scores, expected_scores[method], atol=1e-08) + + +def test_cleanlearning(): + # test fit and predict + cl = CleanLearning() + cl.fit(X, y) + preds = cl.predict(X) + cl_r2_score = cl.score(X, y) + manual_r2_score = r2_score(y, preds) + assert len(preds) == len(y) + assert isinstance(cl_r2_score, float) + assert cl_r2_score == manual_r2_score + + # check if label issues were identified + label_issues = cl.get_label_issues() + identified_label_issues = label_issues[label_issues["is_label_issue"] == True].index + frac_errors_identified = np.mean([e in identified_label_issues for e in error_idx]) + assert frac_errors_identified >= 0.9 # assert most errors were detected + + # compare perf to base LinearRegression model + cl_score = cl.score(X_test, y_test) + lr = LinearRegression() + lr.fit(X, y) + lr_score = lr.score(X_test, y_test) + assert cl_score > lr_score + + # test passing in label issues in various forms + # also test different regression model + cl = CleanLearning(model=SVR()) + label_issues = cl.find_label_issues(X, y) + assert isinstance(label_issues, pd.DataFrame) + + cl.fit(X, y, label_issues=label_issues) + cl.fit(X, pd.Series(y), label_issues=label_issues["is_label_issue"]) + cl.fit(X, list(y), label_issues=label_issues["is_label_issue"].values) + + +def test_optional_inputs(): + # test with sample_weight input + cl = CleanLearning(verbose=1) + cl.fit(X, y, sample_weight=np.random.random(size=len(y))) + cl.fit(X, y, label_issues=cl.get_label_issues(), sample_weight=np.random.random(size=len(y))) + + # test with uncertainty input + cl = CleanLearning() + cl.find_label_issues(X, y, uncertainty=5) # constant uncertainty + cl.find_label_issues(X, y, uncertainty=np.random.random(size=len(y))) # per-example uncertainty + + # test with not calculating uncertainty + cl = CleanLearning(n_boot=0, include_aleatoric_uncertainty=False) + cl.find_label_issues(X, y) + + # test with odd grid search sizes + cl = CleanLearning() + cl.find_label_issues(X, y, coarse_search_range=[0.2]) + cl.find_label_issues(X, y, fine_search_size=0) + cl.fit( + X, y, find_label_issues_kwargs={"coarse_search_range": [0.2, 0.1], "fine_search_size": 2} + ) + + +def test_low_example_count(): + data_tiny = make_data(num_examples=3) + X_tiny, y_tiny = data_tiny["X"], data_tiny["y"] + + try: + cl = CleanLearning() + cl.find_label_issues(X_tiny, y_tiny) + except ValueError as e: + assert "There are too few examples" in str(e) + + cl = CleanLearning(cv_n_folds=3) + cl.find_label_issues(X_tiny, y_tiny) + assert isinstance(cl.get_label_issues(), pd.DataFrame) + + +@pytest.mark.filterwarnings("ignore::UserWarning") +def test_save_space(): + # test label issues df does not save + cl = CleanLearning() + cl.find_label_issues(X, y, save_space=True) + assert cl.get_label_issues() is None + + # test label issues df deletes properly + cl = CleanLearning() + cl.find_label_issues(X, y) + assert isinstance(cl.get_label_issues(), pd.DataFrame) + + cl.save_space() + assert cl.get_label_issues() is None diff --git a/tests/test_segmentation.py b/tests/test_segmentation.py new file mode 100644 index 0000000000..ab686e2fb4 --- /dev/null +++ b/tests/test_segmentation.py @@ -0,0 +1,451 @@ +# Copyright (C) 2017-2023 Cleanlab Inc. +# This file is part of cleanlab. +# +# cleanlab is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cleanlab is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with cleanlab. If not, see . + +""" +Scripts to test cleanlab.segmentation package +""" +import numpy as np + +# import matplotlib.pyplot as plt +# import os +import numpy as np +import random + +np.random.seed(0) +import pytest +from unittest import mock +import matplotlib.pyplot as plt + +from cleanlab.internal.multilabel_scorer import softmin + + +# Segmentation utils +from cleanlab.internal.segmentation_utils import ( + _check_input, + _get_valid_optional_params, + _get_summary_optional_params, +) + +# Filter +from cleanlab.segmentation.filter import ( + find_label_issues, +) + +# Rank +from cleanlab.segmentation.rank import ( + get_label_quality_scores, + issues_from_scores, + _get_label_quality_per_image, +) + +# Summary +from cleanlab.segmentation.summary import ( + display_issues, + common_label_issues, + filter_by_class, + _generate_colormap, +) + + +def generate_three_image_dataset(bad_index): + good_gt = np.zeros((10, 10)) + good_gt[:5, :] = 1.0 + bad_gt = np.ones((10, 10)) + bad_gt[:5, :] = 0.0 + good_pr = np.random.random((2, 10, 10)) + good_pr[0, :5, :] = good_pr[0, :5, :] / 10 + good_pr[1, 5:, :] = good_pr[1, 5:, :] / 10 + + val = np.binary_repr([4, 2, 1][bad_index], width=3) + error = [int(case) for case in val] + + labels = [] + pred = [] + for case in val: + if case == "0": + labels.append(good_gt) + pred.append(good_pr) + else: + labels.append(bad_gt) + pred.append(good_pr) + + labels = np.array(labels) + pred_probs = np.array(pred) + return labels, pred_probs, error + + +labels, pred_probs, error = generate_three_image_dataset(random.randint(0, 2)) +labels, pred_probs = labels.astype(int), pred_probs.astype(float) +num_images, num_classes, h, w = pred_probs.shape + + +def test_find_label_issues(): + issues = find_label_issues(labels, pred_probs, n_jobs=None, batch_size=1000) + assert np.argmax(error) == np.argmax(issues.sum((1, 2))) + + issues = find_label_issues(labels, pred_probs, downsample=2, batch_size=1739) + assert np.argmax(error) == np.argmax(issues.sum((1, 2))) + + issues = find_label_issues(labels, pred_probs, downsample=5, n_jobs=None, batch_size=2838) + assert np.argmax(error) == np.argmax(issues.sum((1, 2))) + + with pytest.raises(Exception) as e: + issues = find_label_issues(labels, pred_probs, downsample=4, n_jobs=None, batch_size=1000) + + # Simple tests + # Test case 1: Test with larger batch_size + issues = find_label_issues(labels, pred_probs, downsample=1, n_jobs=None, batch_size=2000) + assert np.argmax(error) == np.argmax(issues.sum((1, 2))) + + # Test case 2: Test with smaller batch_size + issues = find_label_issues(labels, pred_probs, downsample=1, n_jobs=None, batch_size=500) + assert np.argmax(error) == np.argmax(issues.sum((1, 2))) + + # Test case 3: Test verbose off + issues = find_label_issues(labels, pred_probs, downsample=1, verbose=False) + + assert np.argmax(error) == np.argmax(issues.sum((1, 2))) + + # Test case 5: Test with invalid downsample value + with pytest.raises(Exception) as e: + issues = find_label_issues(labels, pred_probs, downsample=3, n_jobs=None, batch_size=1000) + + # Test case 6: Test with n_jobs parameter + issues = find_label_issues(labels, pred_probs, downsample=1, n_jobs=2, batch_size=1000) + assert np.argmax(error) == np.argmax(issues.sum((1, 2))) + + # Test case 7: Test with invalid labels + with pytest.raises(Exception) as e: + issues = find_label_issues( + np.array([[[[1, 2, 3]]]]), pred_probs, downsample=1, n_jobs=None, batch_size=1000 + ) + + # Test case 8: Test with invalid pred_probs + with pytest.raises(Exception) as e: + issues = find_label_issues( + labels, np.array([[[[0.1, 0.2, 0.3]]]]), downsample=1, n_jobs=None, batch_size=1000 + ) + + +def test_find_label_issues_sizes(): + # checks inputs of different sizes + labels, pred_probs = np.random.randint(0, 2, (2, 9, 7)), np.random.random((2, 2, 9, 7)) + issues = find_label_issues(labels, pred_probs) + + labels, pred_probs = np.random.randint(0, 2, (2, 13, 47)), np.random.random((2, 2, 13, 47)) + issues = find_label_issues(labels, pred_probs) + + for _ in range(5): + h, w = np.random.randint(1, 100, 2) + labels, pred_probs = np.random.randint(0, 2, (2, h, w)), np.random.random((2, 2, h, w)) + issues = find_label_issues(labels, pred_probs) + + +def test__check_input(): + bad_gt = np.random.random((5, 10, 20)) + with pytest.raises(Exception) as e: + _check_input(bad_gt, bad_gt) + + bad_pr = np.random.random((5, 2, 10, 20)) + with pytest.raises(Exception) as e: + _check_input(bad_pr, bad_pr) + + smaller_pr = np.random.random((5, 2, 9, 20)) + with pytest.raises(Exception) as e: + _check_input(bad_gt, smaller_pr) + + fewer_gt = np.random.random((4, 10, 20)) + with pytest.raises(Exception) as e: + _check_input(fewer_gt, smaller_pr) + + +@pytest.mark.filterwarnings("ignore::UserWarning") # Should be 1 warning +def test_get_label_quality_scores(): + image_scores_softmin, pixel_scores = get_label_quality_scores( + labels, pred_probs, method="softmin" + ) + assert np.argmax(error) == np.argmin(image_scores_softmin) + + with pytest.raises(Exception) as e: + get_label_quality_scores(labels, pred_probs, method="num_pixel_issues", downsample=4) + + image_scores_npi, pixel_scores = get_label_quality_scores( + labels, pred_probs, method="num_pixel_issues", downsample=1 + ) + + assert np.argmax(error) == np.argmin(image_scores_npi) + + with pytest.raises(Exception): + get_label_quality_scores(labels, pred_probs, method="invalid_method") + + image_scores_softmin, pixel_scores = get_label_quality_scores( + labels, pred_probs, downsample=1, method="softmin" + ) + + assert len(image_scores_softmin) == labels.shape[0] + assert pixel_scores.shape == labels.shape + + with pytest.raises(ValueError): + get_label_quality_scores(labels, pred_probs, method="num_pixel_issues", batch_size=-1) + get_label_quality_scores( + labels, pred_probs, method="num_pixel_issues", downsample=1, batch_size=0 + ) + + +# different size inpits +def test_get_label_quality_scores_sizes(): + # checks inputs of different sizes + labels, pred_probs = np.random.randint(0, 2, (2, 9, 7)), np.random.random((2, 2, 9, 7)) + image_scores_softmin, pixel_scores = get_label_quality_scores( + labels, pred_probs, method="softmin" + ) + + labels, pred_probs = np.random.randint(0, 2, (2, 13, 47)), np.random.random((2, 2, 13, 47)) + image_scores_softmin, pixel_scores = get_label_quality_scores( + labels, pred_probs, method="softmin" + ) + + for _ in range(5): + h, w = np.random.randint(1, 100, 2) + labels, pred_probs = np.random.randint(0, 2, (2, h, w)), np.random.random((2, 2, h, w)) + image_scores_softmin, pixel_scores = get_label_quality_scores( + labels, pred_probs, method="softmin" + ) + + +# Testing issues from scores +def test_issues_from_scores(): + image_scores_softmin, pixel_scores = get_label_quality_scores( + labels, pred_probs, method="softmin" + ) + issues_from_score = issues_from_scores(image_scores_softmin, pixel_scores, threshold=1) + assert np.shape(issues_from_score) == np.shape(pixel_scores) + assert h * w * num_images == issues_from_score.sum() + + issues_from_score = issues_from_scores(image_scores_softmin, pixel_scores, threshold=0) + assert 0 == issues_from_score.sum() + + issues_from_score = issues_from_scores(image_scores_softmin, pixel_scores, threshold=0.5) + assert np.argmax(error) == np.argmax(issues_from_score.sum((1, 2))) + + sort_by_score = issues_from_scores(image_scores_softmin, threshold=0.5) + assert error[sort_by_score[0]] == 1 + + +def test_issues_from_scores_no_pixel_scores(): + # Test if function works correctly when pixel_scores is None + image_scores_softmin, _ = get_label_quality_scores(labels, pred_probs, method="softmin") + issues_from_score_result = issues_from_scores(image_scores_softmin, None, threshold=1) + assert np.shape(issues_from_score_result) == (num_images,) + + +def test_issues_from_scores_various_thresholds(): + # Test if function works correctly for various values of threshold + image_scores_softmin, pixel_scores = get_label_quality_scores( + labels, pred_probs, method="softmin" + ) + for threshold in [0.1, 0.5, 0.9]: + issues_from_score_result = issues_from_scores( + image_scores_softmin, pixel_scores, threshold=threshold + ) + assert np.all(issues_from_score_result == (pixel_scores < threshold)) + + +def test_issues_from_scores_invalid_inputs(): + # Test if function raises exception when input parameters are invalid + with pytest.raises(ValueError): + issues_from_scores(None) + with pytest.raises(ValueError): + issues_from_scores(np.array([0.1, 0.2, 0.3]), threshold=1.1) # Threshold more than 1 + with pytest.raises(ValueError): + issues_from_scores(np.array([0.1, 0.2, 0.3]), threshold=-0.1) # Threshold less than 0 + + +def test_issues_from_scores_different_input_sizes(): + # Test if function works correctly for different sizes of input arrays + for num_images in range(1, 5): + image_scores = np.random.rand(num_images) + pixel_scores = np.random.rand(num_images, 100, 100) + issues_from_score_result = issues_from_scores(image_scores, pixel_scores, threshold=0.5) + assert np.shape(issues_from_score_result) == np.shape(pixel_scores) + + +def test_issues_from_scores_sorting(): + # Test if function correctly sorts image_scores + image_scores_softmin, _ = get_label_quality_scores(labels, pred_probs, method="softmin") + issues_from_score_result = issues_from_scores(image_scores_softmin, None, threshold=0.5) + assert np.all(np.sort(image_scores_softmin) == image_scores_softmin[issues_from_score_result]) + + +def test__get_label_quality_per_image(): + # Test when pixel_scores is a random list of 100 values, method is "softmin", and temperature is random + random_score_array = np.random.random((100,)) + temp = random.random() + score = _get_label_quality_per_image(random_score_array, method="softmin", temperature=temp) + + cleanlab_softmin = softmin( + np.expand_dims(random_score_array, axis=0), axis=1, temperature=temp + )[0] + assert cleanlab_softmin == score, "Expected cleanlab_softmin to be equal to score" + + # Test when pixel_scores is an empty list, should raise an error + empty_score_array = np.array([]) + with pytest.raises(Exception) as e: + _get_label_quality_per_image(empty_score_array, method="softmin", temperature=temp) + + # Test when method is None + with pytest.raises(Exception): + _get_label_quality_per_image(random_score_array, method=None, temperature=temp) + + # Test when method is not "softmin", should raise an exception + with pytest.raises(Exception): + _get_label_quality_per_image(random_score_array, method="invalid_method", temperature=temp) + + # Test when temperature is 0, should raise an error + with pytest.raises(Exception): + _get_label_quality_per_image(random_score_array, method="softmin", temperature=0) + + with pytest.raises(Exception): + _get_label_quality_per_image(random_score_array, method="softmin", temperature=None) + + +def test_generate_color_map(): + colors = _generate_colormap(0) + assert len(colors) == 0 + + colors = _generate_colormap(1) + assert len(colors) == 1 + assert len(colors[0]) == 4 + + colors = _generate_colormap(-1) + assert len(colors) == 0 + + colors = _generate_colormap(5) + assert len(colors) == 5 + + # test unique + num_colors = 385 # max number of unique colors avalible + colors = _generate_colormap(num_colors) + unique_rows = np.unique(colors, axis=0) + assert unique_rows.shape[0] == num_colors + + +def test_display_issues(monkeypatch): + monkeypatch.setattr(plt, "show", lambda: None) + issues = find_label_issues(labels, pred_probs, downsample=1, n_jobs=None, batch_size=1000) + display_issues(issues, top=1) + + display_issues(issues, pred_probs=pred_probs, labels=labels, top=2, class_names=["one", "two"]) + + display_issues(issues, pred_probs=pred_probs, labels=labels, top=2) + display_issues(issues, labels=labels, top=2) + display_issues(issues, pred_probs=pred_probs, top=2) + + # too many issues for top + display_issues(issues, pred_probs=pred_probs, labels=labels, top=len(issues) + 5) + + class_issues = filter_by_class(0, issues, labels=labels, pred_probs=pred_probs) + display_issues(class_issues, pred_probs=pred_probs, labels=labels, top=2) + + image_scores, pixel_scores = image_scores_softmin, pixel_scores = get_label_quality_scores( + labels, pred_probs, method="softmin" + ) + issues_from_score = issues_from_scores(image_scores, pixel_scores, threshold=0.5) + display_issues(issues_from_score, pred_probs=pred_probs, labels=labels, top=2) + + display_issues(issues_from_score, pred_probs=pred_probs, labels=labels, top=2, exclude=[0]) + + with pytest.raises(ValueError) as e: + display_issues(issues_from_score, pred_probs=pred_probs, labels=None, top=2, exclude=[0]) + + +@mock.patch("matplotlib.pyplot.figure") +def test_display_issues_figure(mock_plt, monkeypatch): + monkeypatch.setattr(plt, "show", lambda: None) + issues = find_label_issues(labels, pred_probs, downsample=1, n_jobs=None, batch_size=1000) + display_issues(issues, pred_probs=pred_probs, labels=labels, top=2, class_names=["one", "two"]) + assert mock_plt.called + + +@mock.patch("matplotlib.pyplot.show") +def test_display_issues_show(mock_plt): + issues = find_label_issues(labels, pred_probs, downsample=1, n_jobs=None, batch_size=1000) + display_issues(issues, top=1) + assert mock_plt.called + + +def test_common_label_issues(capsys): + issues = find_label_issues(labels, pred_probs, downsample=1, n_jobs=None, batch_size=1000) + + # test exclude + df = common_label_issues(issues, labels, pred_probs) + df_without_0 = common_label_issues(issues, labels, pred_probs, exclude=[0]) + assert df.shape != df_without_0.shape + + # test verbose + captured_words = capsys.readouterr() + df = common_label_issues(issues, labels, pred_probs, verbose=False) + captured_no_words = capsys.readouterr() + assert len(captured_no_words.out) == 0 + assert len(captured_words.out) > 0 + + # test class names & top + df_class_names = common_label_issues(issues, labels, pred_probs, class_names=["one", "two"]) + captured_top_all = capsys.readouterr() + df_top_1 = common_label_issues(issues, labels, pred_probs, top=1) + captured_top_1 = capsys.readouterr() + assert len(captured_top_1.out) < len(captured_top_all.out) + assert df_class_names["given_label"].to_list() != df["given_label"].to_list() + + +def test_filter_by_class(): + issues = find_label_issues(labels, pred_probs, downsample=1, n_jobs=None, batch_size=1000) + class_0_issues = filter_by_class(0, issues, labels=labels, pred_probs=pred_probs) + class_1_issues = filter_by_class(1, issues, labels=labels, pred_probs=pred_probs) + + class_300_issues = filter_by_class(300, issues, labels=labels, pred_probs=pred_probs) + + assert (class_0_issues == class_1_issues).all() # mirror issues + assert np.sum(class_300_issues) == 0 + + +def test_summary_sizes(monkeypatch): + monkeypatch.setattr(plt, "show", lambda: None) + + for _ in range(5): + h, w = np.random.randint(1, 100, 2) + labels, pred_probs = np.random.randint(0, 2, (2, h, w)), np.random.random((2, 2, h, w)) + issues = find_label_issues(labels, pred_probs, downsample=1, n_jobs=None, batch_size=1000) + class_300_issues = filter_by_class(0, issues, labels=labels, pred_probs=pred_probs) + df = common_label_issues(issues, labels, pred_probs) + display_issues(issues) + + +def test_get_valid_functions(): + optional_batch_size = 10 + optional_n_jobs = 2 + x, y = _get_valid_optional_params(optional_batch_size, optional_n_jobs) + assert x == optional_batch_size and y == optional_n_jobs + x, y = _get_valid_optional_params(None, None) + assert x == 10000 and y == None + + optional_class_names = [1, 2] + optional_exclude = [1] + optional_top = 10 + x, y, z = _get_summary_optional_params(optional_class_names, optional_exclude, optional_top) + assert x == optional_class_names and y == optional_exclude and z == optional_top + x, y, z = _get_summary_optional_params(None, None, None) + assert x == None and y == [] and z == 20 diff --git a/tests/test_token_classification.py b/tests/test_token_classification.py index 6a72b4f701..42b41fd2c5 100644 --- a/tests/test_token_classification.py +++ b/tests/test_token_classification.py @@ -220,7 +220,7 @@ def test_find_label_issues(test_labels): assert len(issues) == 1 assert issues[0] == (1, 0) issues2 = find_label_issues( - test_labels, pred_probs, return_indices_ranked_by="normalized_margin" + test_labels, pred_probs, return_indices_ranked_by="normalized_margin", n_jobs=1 ) assert isinstance(issues2, list) diff --git a/tests/test_util.py b/tests/test_util.py index 4dd4761c62..78ebe8d3df 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -2,9 +2,11 @@ from cleanlab.internal import util import numpy as np +import pytest -from cleanlab.internal.util import num_unique_classes, format_labels, get_missing_classes +from cleanlab.internal.label_quality_utils import get_normalized_entropy from cleanlab.internal.multilabel_utils import int2onehot, onehot2int +from cleanlab.internal.util import num_unique_classes, format_labels, get_missing_classes from cleanlab.internal.validation import assert_valid_class_labels @@ -146,3 +148,25 @@ def test_format_labels(): assert label_map[1] == "b" assert label_map[2] == "c" assert_valid_class_labels(labels) + + +def test_normalized_entropy(): + """Check that normalized entropy is well well-behaved and in [0, 1].""" + # test tiny numbers + for dtype in [np.float16, np.float32, np.float64]: + info = np.finfo(dtype) + # some NumPy versions have bugs, therefore we provide a fallback + # (fallback is the value of the smalles datatype float16) + smallest_normal = getattr(info, "smallest_normal", 6.104e-05) + smallest_subnormal = getattr(info, "smallest_subnormal", 6e-08) + for val in [info.eps, smallest_normal, smallest_subnormal, 0]: + entropy = get_normalized_entropy(np.array([[1.0, val]], dtype=dtype)) + assert 0.0 <= entropy <= 1.0 + # test multiple _assert_valid_inputs + entropy = get_normalized_entropy(np.array([[0.0, 1.0], [0.5, 0.5]])) + assert all((0.0 <= entropy) & (entropy <= 1.0)) + + # raise errors for wrong probabilities. + with pytest.raises(ValueError): + get_normalized_entropy(np.array([[-1.0, 0.5]])) # negative + get_normalized_entropy(np.array([[2.0, 0.5]])) # larger 1