├── .github └── workflows │ ├── build_wheels_linux.yml │ ├── build_wheels_macos.yml │ ├── delete_artifacts.yml │ ├── test_osx.yml │ ├── tests_linux.yml │ └── tests_windows.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── dev-requirements.txt ├── pypi ├── old_setup.py └── publish_pypi.py ├── pyproject.toml ├── pyxai ├── .gitignore ├── Builder.py ├── Dockerfile ├── Explainer.py ├── LICENCE ├── Learning.py ├── README.md ├── Tools.py ├── __init__.py ├── __main__.py ├── clean.sh ├── convert.py ├── docker │ └── pyxai-experimental │ │ ├── .virtual_documents │ │ └── Untitled.ipynb │ │ ├── Dockerfile │ │ └── Untitled.ipynb ├── examples │ ├── BT │ │ ├── GUI-mnist38.py │ │ ├── builder-simple.py │ │ ├── regression │ │ │ ├── builder-regression.py │ │ │ └── simple-regression.py │ │ └── simple.py │ ├── Converters │ │ ├── bad │ │ │ └── converter-dorothea.py │ │ ├── converter-adult.py │ │ ├── converter-arrowhead.py │ │ ├── converter-australian.py │ │ ├── converter-balance-scale.py │ │ ├── converter-bank.py │ │ ├── converter-biodegradation.py │ │ ├── converter-breast-tumor.py │ │ ├── converter-bupa.py │ │ ├── converter-christine.py │ │ ├── converter-cifar.py │ │ ├── converter-cleveland.py │ │ ├── converter-cnae.py │ │ ├── converter-compas.py │ │ ├── converter-contraceptive.py │ │ ├── converter-default-paiement.py │ │ ├── converter-dexter.py │ │ ├── converter-divorce.py │ │ ├── converter-german.py │ │ ├── converter-gisette.py │ │ ├── converter-melb.py │ │ ├── converter-mnist.py │ │ ├── converter-nerve.py │ │ ├── converter-regression.py │ │ ├── converter-spambase.py │ │ └── converter-wine.py │ ├── DT │ │ ├── GUI-cifar.py │ │ ├── GUI-mnist38-contrastives.py │ │ ├── GUI-mnist38.py │ │ ├── base.py │ │ ├── builder-anchored.py │ │ ├── builder-loan.py │ │ ├── builder-orchid.py │ │ ├── builder-rectify1.py │ │ ├── builder-rectify2.py │ │ ├── builder-rectify3.py │ │ └── simple.py │ ├── RF │ │ ├── GUI-australian.py │ │ ├── GUI-builder-loan.py │ │ ├── GUI-mnist49.py │ │ ├── builder-anchored.py │ │ ├── builder-anchored2.py │ │ ├── builder-categorical-paper.py │ │ ├── builder-categorical.py │ │ ├── builder-cattleya.py │ │ ├── builder-loan-simple.py │ │ ├── builder-loan.py │ │ ├── builder-multiclasses.py │ │ ├── simple.py │ │ ├── theories-majoritary.py │ │ ├── theories-types-dict.py │ │ └── theories-types-file.py │ └── xai24 │ │ ├── cases.py │ │ ├── constants.py │ │ ├── coverage.py │ │ ├── main.py │ │ ├── misc.py │ │ ├── model.py │ │ ├── tests.py │ │ └── user.py ├── explanations │ ├── BT_builder_regression.explainer │ ├── BT_builder_simple.explainer │ ├── BT_iris.explainer │ ├── BT_mnist38_tree_specific_class_3.explainer │ ├── BT_mnist38_tree_specific_class_8.explainer │ ├── BT_wine_regression.explainer │ ├── DT_dexter.explainer │ ├── DT_loan.explainer │ ├── DT_mnist38_contrastives.explainer │ ├── DT_mnist38_hot_map.explainer │ ├── DT_orchid.explainer │ ├── DT_wine.explainer │ ├── RF_australian_majoritary.explainer │ ├── RF_australian_theory.explainer │ ├── RF_biodegradation.explainer │ ├── RF_breast_tumor_theory.explainer │ ├── RF_buba.explainer │ ├── RF_cifar_cat_minimal_majoritary.explainer │ ├── RF_cnae.explainer │ ├── RF_iris_majoritary_theory.explainer │ └── RF_mnist49_minimal_majoritary.explainer ├── setup.py ├── sources │ ├── __init__.py │ ├── core │ │ ├── __init__.py │ │ ├── explainer │ │ │ ├── Explainer.py │ │ │ ├── Visualisation.py │ │ │ ├── __init__.py │ │ │ ├── explainerBT.py │ │ │ ├── explainerDT.py │ │ │ ├── explainerRF.py │ │ │ └── explainerRegressionBT.py │ │ ├── structure │ │ │ ├── __init__.py │ │ │ ├── binaryMapping.py │ │ │ ├── boostedTrees.py │ │ │ ├── decisionNode.py │ │ │ ├── decisionTree.py │ │ │ ├── randomForest.py │ │ │ ├── treeEnsembles.py │ │ │ └── type.py │ │ └── tools │ │ │ ├── GUIQT.py │ │ │ ├── __init__.py │ │ │ ├── encoding.py │ │ │ ├── option.py │ │ │ ├── utils.py │ │ │ └── vizualisation.py │ ├── learning │ │ ├── __init__.py │ │ ├── generic.py │ │ ├── learner.py │ │ ├── learner_information.py │ │ ├── lightgbm.py │ │ ├── preprocessor.py │ │ ├── scikitlearn.py │ │ └── xgboost.py │ └── solvers │ │ ├── COMPILER │ │ ├── D4Solver.py │ │ └── d4_static │ │ ├── CSP │ │ ├── AbductiveV1.py │ │ ├── AbductiveV2.py │ │ ├── TSMinimalV1.py │ │ ├── TSMinimalV2.py │ │ └── TSMinimalV3.py │ │ ├── ENCORE │ │ ├── ENCORESolver.py │ │ └── __init__.py │ │ ├── GRAPH │ │ └── TreeDecomposition.py │ │ ├── GREEDY │ │ ├── CMakeLists.txt │ │ └── src │ │ │ ├── Explainer.cc │ │ │ ├── Explainer.h │ │ │ ├── Node.cc │ │ │ ├── Node.h │ │ │ ├── Rectifier.cc │ │ │ ├── Rectifier.h │ │ │ ├── Tree.cc │ │ │ ├── Tree.h │ │ │ ├── bcp │ │ │ ├── BufferRead.h │ │ │ ├── FactoryException.h │ │ │ ├── ParserDimacs.cc │ │ │ ├── ParserDimacs.h │ │ │ ├── Problem.cc │ │ │ ├── Problem.h │ │ │ ├── ProblemTypes.cc │ │ │ ├── ProblemTypes.h │ │ │ ├── Propagator.cc │ │ │ └── Propagator.h │ │ │ ├── bt_wrapper.cc │ │ │ ├── constants.h │ │ │ └── utils │ │ │ └── TimerHelper.h │ │ ├── MAXSAT │ │ ├── MAXSATSolver.py │ │ ├── OPENWBOSolver.py │ │ ├── RC2solver.py │ │ ├── __init__.py │ │ ├── openwbo-darwin │ │ ├── openwbo-linux │ │ └── openwbo-windows │ │ ├── MIP │ │ ├── ContrastiveBT.py │ │ ├── Range.py │ │ ├── SufficientRegressionBT.py │ │ ├── __init__.py │ │ └── help_functions.py │ │ ├── MUS │ │ ├── MUSERSolver.py │ │ ├── OPTUXSolver.py │ │ ├── __init__.py │ │ └── exec │ │ │ └── muser-linux │ │ ├── SAT │ │ └── glucoseSolver.py │ │ ├── __init__.py │ │ └── old_exec │ │ └── muser-darwin └── tests │ ├── __init__.py │ ├── compas.csv │ ├── dermatology.csv │ ├── explaining │ ├── __init__.py │ ├── bt.py │ ├── dt.py │ ├── misc.py │ ├── regressionbt.py │ ├── rf-multiclasses.py │ └── rf.py │ ├── functionality │ ├── GetInstances.py │ ├── Metrics.py │ ├── Rectify.py │ ├── ToFeatures.py │ └── __init__.py │ ├── importing │ ├── LightGBM.py │ ├── ScikitLearn.py │ ├── SimpleScikitLearn.py │ ├── XGBoost.py │ └── __init__.py │ ├── iris.csv │ ├── learning │ ├── LightGBM.py │ ├── ScikitLearn.py │ ├── XGBoost.py │ └── __init__.py │ ├── saveload │ ├── LightGBM.py │ ├── ScikitLearn.py │ ├── XGBoost.py │ └── __init__.py │ ├── test_inverse_class.csv │ ├── tests.py │ └── winequality-red.csv └── setup.py /.github/workflows/build_wheels_linux.yml: -------------------------------------------------------------------------------- 1 | name: Build Linux 2 | 3 | on: [workflow_dispatch] 4 | 5 | jobs: 6 | build_wheels: 7 | name: Build wheels on ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | # Ensure that a wheel builder finishes even if another fails 11 | fail-fast: false 12 | matrix: 13 | os: [ubuntu-latest, ubuntu-24.04-arm] 14 | #os: [ubuntu-20.04, windows-2019, macOS-11] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | # Used to host cibuildwheel 20 | - uses: actions/setup-python@v5 21 | 22 | - name: Install cibuildwheel 23 | run: python -m pip install cibuildwheel 24 | #run: python -m pip install cibuildwheel==2.16.2 25 | 26 | - name: Build wheels 27 | run: python -m cibuildwheel --output-dir wheelhouse 28 | # to supply options, put them in 'env', like: 29 | # env: 30 | # CIBW_SOME_OPTION: value 31 | 32 | - uses: actions/upload-artifact@v4 33 | with: 34 | name: wheels_${{ matrix.os }} 35 | path: ./wheelhouse/*.whl 36 | -------------------------------------------------------------------------------- /.github/workflows/build_wheels_macos.yml: -------------------------------------------------------------------------------- 1 | name: Build MacOS 2 | 3 | on: [workflow_dispatch] 4 | 5 | jobs: 6 | build_wheels: 7 | name: Build wheels on ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | # Ensure that a wheel builder finishes even if another fails 11 | fail-fast: false 12 | matrix: 13 | os: [macos-13, macos-latest] 14 | #os: [ubuntu-20.04, windows-2019, macOS-11] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | # Used to host cibuildwheel 20 | - uses: actions/setup-python@v5 21 | 22 | - name: Install GCC 23 | run: brew install gcc 24 | 25 | - name: Install cibuildwheel 26 | run: python -m pip install cibuildwheel 27 | 28 | - name: Build wheels 29 | run: python -m cibuildwheel --output-dir wheelhouse 30 | env: 31 | MACOSX_DEPLOYMENT_TARGET: 12.0 32 | #env: 33 | # CIBW_ARCHS_MACOS: arm64 34 | # to supply options, put them in 'env', like: 35 | # env: 36 | # CIBW_SOME_OPTION: value 37 | 38 | - uses: actions/upload-artifact@v4 39 | with: 40 | name: wheels_${{ matrix.os }} 41 | path: ./wheelhouse/*.whl 42 | -------------------------------------------------------------------------------- /.github/workflows/delete_artifacts.yml: -------------------------------------------------------------------------------- 1 | name: Delete Artifacts 2 | 3 | on: [workflow_dispatch] #Manually 4 | 5 | jobs: 6 | job1: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: kolpav/purge-artifacts-action@v1 11 | with: 12 | token: ${{ secrets.GITHUB_TOKEN }} 13 | expire-in: 0 -------------------------------------------------------------------------------- /.github/workflows/test_osx.yml: -------------------------------------------------------------------------------- 1 | name: OSX tests 2 | 3 | on: [workflow_dispatch] #Manually 4 | #on: [push] 5 | #on: 6 | # workflow_run: 7 | # workflows: ["Build"] 8 | # types: 9 | # - completed 10 | 11 | jobs: 12 | tests: 13 | runs-on: macos-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-python@v3 17 | with: 18 | python-version: '3.11' 19 | - name: Build a wheel via an sdist 20 | run: | 21 | brew install libomp 22 | pip install build 23 | python -m build 24 | pip install dist/pyxai*.whl 25 | # sudo apt install ffmpeg libsm6 libxext6 qt6-base-dev libxcb-cursor0 -y 26 | - name: Run test suite 27 | run: | 28 | python3 -m pyxai -tests 29 | -------------------------------------------------------------------------------- /.github/workflows/tests_linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux tests 2 | 3 | on: [workflow_dispatch] #Manually 4 | #on: [push] 5 | #on: 6 | # workflow_run: 7 | # workflows: ["Build"] 8 | # types: 9 | # - completed 10 | 11 | jobs: 12 | tests: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-python@v5 17 | #with: 18 | # python-version: '3.12' 19 | - name: Build a wheel via an sdist 20 | run: | 21 | pip install build 22 | python -m build 23 | pip install dist/pyxai*.whl 24 | sudo apt install ffmpeg libsm6 libxext6 qt6-base-dev libxcb-cursor0 -y 25 | - name: Run test suite 26 | run: | 27 | python3 -u -m pyxai -tests 28 | -------------------------------------------------------------------------------- /.github/workflows/tests_windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows tests 2 | 3 | #on: [push] 4 | on: [workflow_dispatch] #Manually 5 | #on: 6 | # workflow_run: 7 | # workflows: ["Build"] 8 | # types: 9 | # - completed 10 | 11 | jobs: 12 | tests: 13 | runs-on: windows-2019 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-python@v3 17 | - name: Build a wheel via an sdist 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install build 21 | python -m build 22 | pip install dist/pyxai-1.0.7-cp311-cp311-win_amd64.whl 23 | #sudo apt install ffmpeg libsm6 libxext6 qt6-base-dev libxcb-cursor0 -y 24 | - name: Run test suite 25 | run: | 26 | python3 -m pyxai -tests -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | *.pyc 3 | *~ 4 | *# 5 | #* 6 | .#* 7 | .spyproject/* 8 | .idea 9 | *pycache* 10 | *.*~ 11 | *.*# 12 | *.aux 13 | *.log 14 | *.nav 15 | *.out 16 | *.snm 17 | *.toc 18 | *.vrb 19 | *.so 20 | build 21 | dist 22 | requirements-download/ 23 | *.xml 24 | wheelhouse 25 | pyxai.egg-info/ 26 | pyxai_experimental.egg-info/ 27 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "pyxai"] 2 | path = pyxai 3 | url = https://gitlab.univ-artois.fr/pyxai/pyxai-experimental.git 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 1.0.10 4 | - Contrastive for BT classification (binary classes) 5 | - Remove the PyQt6 dependencie and new methods to display explanations: 6 | - show_in_notebook() 7 | - show_on_screen() 8 | - get_PILImage() 9 | - save_png() 10 | - resize_PILimage() 11 | - Change function name in explainer (unset_specific_features -> unset_excluded_features) 12 | - New procedure installation 13 | - New visualization for time series 14 | - Compilation error resolution 15 | 16 | ### 1.0.9 17 | - New metrics (documentation in progress) 18 | - For binary classification: 19 | - accuracy 20 | - precision 21 | - recall 22 | - f1_score 23 | - specificity 24 | - tp, tn, fp, fn 25 | - For multiclass classification: 26 | - micro_averaging_accuracy 27 | - micro_averaging_precision 28 | - micro_averaging_recall 29 | - macro_averaging_accuracy 30 | - macro_averaging_precision 31 | - macro_averaging_recall 32 | - For regression: 33 | - mean_squared_error 34 | - root_mean_squared_error 35 | - mean_absolute_error 36 | 37 | ### 1.0.7 38 | - Build and Tests with CI 39 | 40 | ### 1.0.0 41 | - Regression for boosted trees 42 | - Adding thoeries 43 | - Easier import model 44 | - Graphical user interface: displaying, loading, saving explanations 45 | - Data preprocessing 46 | - unit tests 47 | ## 0.X 48 | - Initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 CRIL 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | 2 | include pyxai/version.txt 3 | 4 | recursive-include pyxai/sources/solvers/COMPILER * 5 | recursive-include pyxai/sources/solvers/CPP_CODE * 6 | recursive-include pyxai/sources/solvers/CSP * 7 | recursive-include pyxai/sources/solvers/GRAPH * 8 | recursive-include pyxai/sources/solvers/MAXSAT * 9 | recursive-include pyxai/sources/solvers/MUS * 10 | recursive-include pyxai/sources/solvers/SAT * 11 | 12 | recursive-include pyxai/examples * 13 | recursive-include pyxai/explanations * 14 | recursive-include pyxai/tests * 15 | 16 | global-exclude __pycache__ 17 | global-exclude *.pyc 18 | prune __pycache__ 19 | prune *.pyc 20 | 21 | graft pyxai/sources/solvers/GREEDY/src -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | nose 2 | -------------------------------------------------------------------------------- /pypi/old_setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from distutils.core import Extension 3 | from setuptools import setup, find_packages 4 | 5 | __version__ = open(os.path.join(os.path.dirname(__file__), 'pyxai/version.txt'), encoding='utf-8').read() 6 | __cxx_path__ = "pyxai/sources/solvers/GREEDY/src/" 7 | __cxx_files__ = [__cxx_path__+f for f in os.listdir(__cxx_path__) if f.endswith(".cc")]+[__cxx_path__+"bcp/"+f for f in os.listdir(__cxx_path__+"bcp/") if f.endswith(".cc")] 8 | __cxx_headers__ = [__cxx_path__+f for f in os.listdir(__cxx_path__) if f.endswith(".h")]+[__cxx_path__+"bcp/"+f for f in os.listdir(__cxx_path__+"bcp/") if f.endswith(".h")] 9 | 10 | print("__version__", __version__) 11 | print("__cxx_path__:", __cxx_path__) 12 | print("__cxx_files__:", __cxx_files__) 13 | print("__cxx_headers__:", __cxx_headers__) 14 | 15 | setup(name='pyxai', 16 | version=__version__, 17 | python_requires='>=3.6', 18 | project_urls={ 19 | 'Documentation': 'http://www.cril.univ-artois.fr/pyxai/', 20 | 'Installation': 'http://www.cril.univ-artois.fr/pyxai/documentation/installation/', 21 | 'Git': 'https://github.com/crillab/pyxai' 22 | }, 23 | author='Gilles Audemard, Steve Bellart, Louenas Bounia, Jean-Marie Lagniez, Pierre Marquis, Nicolas Szczepanski:', 24 | author_email='audemard@cril.fr, bellart@cril.fr, bounia@cril.fr, lagniez@cril.fr, marquis@cril.fr, szczepanski@cril.fr', 25 | maintainer='Gilles Audemard, Nicolas Szczepanski', 26 | maintainer_email='audemard@cril.fr, szczepanski@cril.fr', 27 | keywords='XAI AI ML explainable learning', 28 | classifiers=['Topic :: Scientific/Engineering :: Artificial Intelligence', 'Topic :: Education'], 29 | packages=find_packages(), # exclude=["problems/g7_todo/"]), 30 | package_dir={'pyxai': 'pyxai'}, 31 | install_requires=['lxml', 'numpy', 'wheel', 'pandas', 'termcolor', 'shap', 'wordfreq', 'python-sat[pblib,aiger]', 'xgboost==1.7.3', 'pycsp3', 'matplotlib', 'dill', 'lightgbm', 'docplex', 'ortools', 'packaging'], 32 | extras_require={ 33 | "gui": ['pyqt6'], 34 | }, 35 | include_package_data=True, 36 | description='Explaining Machine Learning Classifiers in Python', 37 | long_description=open(os.path.join(os.path.dirname(__file__), 'README.md'), encoding='utf-8').read(), 38 | long_description_content_type='text/markdown', 39 | license='MIT', 40 | ext_modules=[Extension( 41 | "c_explainer", 42 | __cxx_files__, 43 | language="c++", 44 | extra_compile_args=["-std=c++11"] 45 | )], 46 | headers=__cxx_headers__, 47 | platforms='LINUX') 48 | -------------------------------------------------------------------------------- /pypi/publish_pypi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # How to build, test and publish on pypi a new version: 4 | # - Put the good version in the pyxai/version.txt file 5 | # - Push on the public github of pyxai 6 | # - Two github actions are launch: Build and Tests 7 | # - The github action Build create the wheels for pypi 8 | # - The github action Build create the wheels for pypi 9 | # - This script get the last wheels and publish them on pypi. 10 | 11 | print("Please use the 'gh auth login' command to connect the github API.") 12 | print("Type 'enter' to execute: 'gh run download'") 13 | input() 14 | 15 | os.system("gh run download") 16 | 17 | print("Type 'enter' to publish the wheels on pypi:") 18 | input() 19 | os.system("python3 -m twine upload --skip-existing *.whl") 20 | 21 | print("Type 'enter' to delete the whell:") 22 | os.system("rm -rf *.whl") 23 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pyxai" 7 | requires-python = ">= 3.7" 8 | dynamic = ["version"] 9 | authors = [ 10 | { name = "Gilles Audemard", email = "audemard@cril.fr" }, 11 | { name = "Steve Bellart", email = "bellart@cril.fr" }, 12 | { name = "Louenas Bounia", email = "bounia@cril.fr" }, 13 | { name = "Jean-Marie Lagniez", email = "lagniez@cril.fr" }, 14 | { name = "Pierre Marquis", email = "marquis@cril.fr" }, 15 | { name = "Nicolas Szczepanski", email = "szczepanski@cril.fr" } 16 | ] 17 | maintainers = [ 18 | { name = "Gilles Audemard", email = "audemard@cril.fr" }, 19 | { name = "Nicolas Szczepanski", email = "szczepanski@cril.fr" } 20 | ] 21 | keywords = ["XAI", "AI", "ML", "explainable", "learning"] 22 | classifiers = [ 23 | "Topic :: Scientific/Engineering :: Artificial Intelligence", 24 | "Topic :: Education" 25 | ] 26 | dependencies = [ 27 | "lxml", 28 | "numpy", 29 | "wheel", 30 | "pandas", 31 | "termcolor", 32 | "shap", 33 | "wordfreq", 34 | "python-sat[pblib,aiger]", 35 | "xgboost==1.7.3", 36 | "pycsp3", 37 | "matplotlib", 38 | "dill", 39 | "lightgbm", 40 | "docplex", 41 | "ortools", 42 | "packaging" 43 | ] 44 | optional-dependencies = { gui = ["pyqt6"] } 45 | description = "Explaining Machine Learning Classifiers in Python" 46 | readme = "README.md" 47 | license = { text = "MIT" } 48 | #packages = { include = ["pyxai"] } 49 | #include = ["pyxai/sources/solvers/GREEDY/src/*.cc", "pyxai/sources/solvers/GREEDY/src/bcp/*.cc", "pyxai/sources/solvers/GREEDY/src/*.h", "pyxai/sources/solvers/GREEDY/src/bcp/*.h"] 50 | 51 | [project.urls] 52 | Documentation = "http://www.cril.univ-artois.fr/pyxai/" 53 | Installation = "http://www.cril.univ-artois.fr/pyxai/documentation/installation/" 54 | Git = "https://github.com/crillab/pyxai" 55 | 56 | [tool.setuptools] 57 | package-dir = { pyxai = "pyxai" } 58 | include-package-data = true 59 | 60 | [tool.setuptools.packages] 61 | find = {} 62 | 63 | [tool.setuptools.dynamic] 64 | version = { attr = "pyxai.__version__" } 65 | 66 | [tool.cibuildwheel] 67 | build = "*" 68 | skip = "pp38-* pp39-* cp36-* pp310-macosx_* pp311-macosx_*" 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /pyxai/.gitignore: -------------------------------------------------------------------------------- 1 | #from https://github.com/github/gitignore/blob/main/Python.gitignore 2 | 3 | .idea 4 | tmp_diagram.png 5 | .vscode/ 6 | examples/datasets/ 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | *.pyc 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | cover/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | .pybuilder/ 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # IPython 88 | profile_default/ 89 | ipython_config.py 90 | 91 | # pyenv 92 | # For a library or package, you might want to ignore these files since the code is 93 | # intended to run in multiple environments; otherwise, check them in: 94 | # .python-version 95 | 96 | # pipenv 97 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 98 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 99 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 100 | # install all needed dependencies. 101 | #Pipfile.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/#use-with-ide 116 | .pdm.toml 117 | 118 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 119 | __pypackages__/ 120 | 121 | # Celery stuff 122 | celerybeat-schedule 123 | celerybeat.pid 124 | 125 | # SageMath parsed files 126 | *.sage.py 127 | 128 | # Environments 129 | .env 130 | .venv 131 | env/ 132 | venv/ 133 | ENV/ 134 | env.bak/ 135 | venv.bak/ 136 | 137 | # Spyder project settings 138 | .spyderproject 139 | .spyproject 140 | 141 | # Rope project settings 142 | .ropeproject 143 | 144 | # mkdocs documentation 145 | /site 146 | 147 | # mypy 148 | .mypy_cache/ 149 | .dmypy.json 150 | dmypy.json 151 | 152 | # Pyre type checker 153 | .pyre/ 154 | 155 | # pytype static type analyzer 156 | .pytype/ 157 | 158 | # Cython debug symbols 159 | cython_debug/ 160 | 161 | -------------------------------------------------------------------------------- /pyxai/Builder.py: -------------------------------------------------------------------------------- 1 | from pyxai.sources.core.structure.boostedTrees import BoostedTrees, BoostedTreesRegression 2 | from pyxai.sources.core.structure.decisionNode import DecisionNode, LeafNode 3 | from pyxai.sources.core.structure.decisionTree import DecisionTree 4 | from pyxai.sources.core.structure.randomForest import RandomForest 5 | from pyxai.sources.core.structure.type import OperatorCondition 6 | 7 | GE = OperatorCondition.GE 8 | GT = OperatorCondition.GT 9 | LE = OperatorCondition.LE 10 | LT = OperatorCondition.LT 11 | EQ = OperatorCondition.EQ 12 | NEQ = OperatorCondition.NEQ 13 | -------------------------------------------------------------------------------- /pyxai/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | RUN apt update 4 | RUN apt install python3 -y 5 | RUN apt install python3-pip -y 6 | RUN apt install ffmpeg libsm6 libxext6 qt6-base-dev -y 7 | 8 | RUN pip install jupyter 9 | RUN pip install pyxai-experimental 10 | 11 | ADD . /data/ 12 | 13 | WORKDIR /data 14 | 15 | VOLUME /data 16 | 17 | EXPOSE 8888 18 | 19 | CMD jupyter notebook --allow-root --no-browser --ip=0.0.0.0 -------------------------------------------------------------------------------- /pyxai/LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022, Univ. Artois and CNRS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /pyxai/Tools.py: -------------------------------------------------------------------------------- 1 | from pyxai import Options 2 | from pyxai.sources.core.tools.utils import Metric, Stopwatch, verbose, set_verbose 3 | -------------------------------------------------------------------------------- /pyxai/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import shutil 4 | import pyxai 5 | import platform 6 | import subprocess 7 | import unittest 8 | import matplotlib 9 | 10 | __version__ = "1.0.15" 11 | 12 | matplotlib.set_loglevel("critical") #To win a lot of times. 13 | 14 | from pyxai.sources.core.tools.option import Options 15 | from pyxai.sources.core.tools.utils import set_verbose, check_PyQt6 16 | 17 | __python_version__ = str(sys.version).split(os.linesep)[0].split(' ')[0] 18 | __pyxai_location__ = os.sep.join(pyxai.__file__.split(os.sep)[:-1]) 19 | 20 | Options.set_values("dataset", "model_directory", "n_iterations", "time_limit", "verbose", "types", "output", "type_references", "N") 21 | Options.set_flags("f", "gui", "examples", "explanations", "tests") 22 | Options.parse(sys.argv[1:]) 23 | if Options.verbose is not None: set_verbose(Options.verbose) 24 | 25 | if sys.argv: 26 | if (len(sys.argv) != 0 and sys.argv[0] == "-m"): 27 | print("Python version: ", __python_version__) 28 | print("PyXAI version: ", __version__) 29 | print("PyXAI location: ", __pyxai_location__) 30 | 31 | if (len(sys.argv) == 1 and sys.argv[0] == "-m") or (len(sys.argv) == 2 and sys.argv[0] == "-m" and sys.argv[1] == "-gui"): 32 | check_PyQt6() 33 | 34 | from pyxai.sources.core.tools.GUIQT import GraphicalInterface 35 | graphical_interface = GraphicalInterface(None) 36 | graphical_interface.mainloop() 37 | elif (len(sys.argv) == 2 and sys.argv[0] == "-m" and sys.argv[1] == "-examples"): 38 | 39 | 40 | examples = __pyxai_location__ + os.sep + "examples" + os.sep 41 | target = os.getcwd() + os.sep + "examples" + os.sep 42 | print("Source of files found: ", examples) 43 | shutil.copytree(examples, target, ignore=shutil.ignore_patterns('in_progress', '__init__.py', '__pycache__*')) 44 | print("Successful creation of the " + target + " directory containing the examples.") 45 | exit(0) 46 | 47 | elif (len(sys.argv) == 2 and sys.argv[0] == "-m" and sys.argv[1] == "-explanations"): 48 | 49 | explanations = __pyxai_location__ + os.sep + "explanations" + os.sep 50 | target = os.getcwd() + os.sep + "explanations" + os.sep 51 | print("Source of files found: ", explanations) 52 | shutil.copytree(explanations, target, ignore=shutil.ignore_patterns('in_progress', '__init__.py', '__pycache__*')) 53 | print("Successful creation of the " + target + " directory containing the explanations.") 54 | exit(0) 55 | 56 | elif (len(sys.argv) == 2 and sys.argv[0] == "-m" and sys.argv[1] == "-tests"): 57 | save_directory = os.getcwd() 58 | os.chdir(__pyxai_location__) 59 | print("Change directory to PyXAI location: ", __pyxai_location__) 60 | cmd = "python3 -u tests"+os.sep+"tests.py -f" 61 | cmd = cmd.split(" ") 62 | print("command:", cmd) 63 | process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 64 | output, errors = process.communicate() 65 | #stdout = process.stdout.read().decode('utf-8') 66 | #stderr = process.stderr.read().decode('utf-8') 67 | print("stdout:", output.decode('utf-8')) 68 | print("stderr:", errors.decode('utf-8')) 69 | 70 | #print("stderr:", stderr) 71 | #print("return code:", process.returncode) 72 | #print("return code2:", errors) 73 | os.chdir(save_directory) 74 | exit(process.poll()) 75 | #if platform.system() == "Windows": 76 | # exit(status) 77 | #else: 78 | # exit(os.WEXITSTATUS(status)) 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /pyxai/__main__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/__main__.py -------------------------------------------------------------------------------- /pyxai/clean.sh: -------------------------------------------------------------------------------- 1 | 2 | rm -rf *.instance 3 | rm -rf *.start 4 | rm -rf build/ 5 | rm -rf solver_* 6 | rm -rf *.xml 7 | rm -rf *.pyc 8 | rm -rf __pycache__ 9 | rm -rf *.*~ 10 | rm -rf temp/ 11 | -------------------------------------------------------------------------------- /pyxai/convert.py: -------------------------------------------------------------------------------- 1 | #python3 examples/Converters/converter-adult.py -dataset=examples/datasets_not_converted/adult.data 2 | 3 | import sys 4 | import os 5 | import subprocess 6 | import numpy 7 | from statistics import median 8 | 9 | assert len(sys.argv) == 2, "You have to put the dataset directory please !" 10 | dataset = sys.argv[1] 11 | 12 | for filename in os.listdir(dataset): 13 | completename = os.path.join(dataset, filename) 14 | 15 | if not completename.endswith("~"): 16 | print(completename) 17 | name = completename.split("/")[-1].split(".")[0].split("_")[0] 18 | print("name:", name) 19 | if name == "mnist49" or name == "mnist38": 20 | name = "mnist" 21 | command = "python3 examples/Converters/converter-"+name+".py -dataset=" + completename 22 | os.system(command) 23 | input("Press Enter to continue...") 24 | -------------------------------------------------------------------------------- /pyxai/docker/pyxai-experimental/.virtual_documents/Untitled.ipynb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | from pyxai import Explainer 5 | Explainer.show() 6 | 7 | 8 | import sys 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /pyxai/docker/pyxai-experimental/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | RUN apt update 4 | RUN apt install python3 -y 5 | RUN apt install python3-pip -y 6 | RUN apt install ffmpeg libsm6 libxext6 qt6-base-dev libxcb-cursor0 libxcb-xinerama0 nodejs -y 7 | RUN apt install wget -y 8 | 9 | 10 | RUN wget https://repo.anaconda.com/archive/Anaconda3-2023.03-1-Linux-x86_64.sh 11 | RUN bash Anaconda3-2023.03-1-Linux-x86_64.sh -b 12 | ENV PATH="/root/anaconda3/bin:$PATH" 13 | 14 | RUN conda install -c conda-forge nodejs 15 | RUN conda install -c conda-forge/label/gcc7 nodejs 16 | RUN conda install -c conda-forge/label/cf201901 nodejs 17 | RUN conda install -c conda-forge/label/cf202003 nodejs 18 | 19 | RUN jupyter labextension install @jupyter-widgets/jupyterlab-manager 20 | 21 | RUN pip install -U jupyterlab 22 | RUN pip install -U pyxai-experimental 23 | RUN pip install -U ipython 24 | RUN pip install -U widgetsnbextension 25 | RUN pip install -U nodejs 26 | RUN pip install -U ipywidgets 27 | RUN pip install -U jupyter-lsp 28 | 29 | ADD . /data/ 30 | 31 | WORKDIR /data 32 | 33 | VOLUME /data 34 | 35 | EXPOSE 8888 36 | 37 | CMD jupyter lab --allow-root --no-browser --ip=0.0.0.0 -------------------------------------------------------------------------------- /pyxai/docker/pyxai-experimental/Untitled.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "d9e5ae97-d2c1-45ad-86d3-8c1af13f942f", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "ac48f4ae-3230-44a1-b345-c2daf29200e8", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from pyxai import Explainer\n", 19 | "Explainer.show()" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": 1, 25 | "id": "74e0ba65-ac2b-4b5c-b3c6-685af1fdd8d8", 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "import sys\n" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "id": "3e70b995-b387-40d8-84e7-05bc820880a2", 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": null, 43 | "id": "c31ca4eb-b740-4e4d-8885-cab0c1eed6f7", 44 | "metadata": {}, 45 | "outputs": [], 46 | "source": [] 47 | } 48 | ], 49 | "metadata": { 50 | "kernelspec": { 51 | "display_name": "Python 3 (ipykernel)", 52 | "language": "python", 53 | "name": "python3" 54 | }, 55 | "language_info": { 56 | "codemirror_mode": { 57 | "name": "ipython", 58 | "version": 3 59 | }, 60 | "file_extension": ".py", 61 | "mimetype": "text/x-python", 62 | "name": "python", 63 | "nbconvert_exporter": "python", 64 | "pygments_lexer": "ipython3", 65 | "version": "3.10.9" 66 | } 67 | }, 68 | "nbformat": 4, 69 | "nbformat_minor": 5 70 | } 71 | -------------------------------------------------------------------------------- /pyxai/examples/BT/GUI-mnist38.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | from pyxai import Learning, Explainer, Tools 3 | 4 | # To use with the minist dataset for example 5 | # (classification between 4 and 9 or between 3 and 8) 6 | # available here: 7 | # http://www.cril.univ-artois.fr/expekctation/datasets/mnist38.csv 8 | # http://www.cril.univ-artois.fr/expekctation/datasets/mnist49.csv 9 | 10 | # Check V1.0: Ok 11 | 12 | # the location of the dataset 13 | path = "" 14 | dataset = "../../data/dataML/mnist38.csv" 15 | 16 | # Machine learning part 17 | learner = Learning.Xgboost(dataset, learner_type=Learning.CLASSIFICATION) 18 | model = learner.evaluate(method=Learning.HOLD_OUT, output=Learning.BT) 19 | instances = learner.get_instances(model, n=2, correct=True, predictions=[0]) 20 | 21 | # Explanation part 22 | 23 | explainer = Explainer.initialize(model) 24 | for (instance, prediction) in instances: 25 | explainer.set_instance(instance) 26 | 27 | direct = explainer.direct_reason() 28 | print("len direct:", len(direct)) 29 | 30 | tree_specific_reason = explainer.tree_specific_reason() 31 | print("len tree_specific_reason:", len(tree_specific_reason)) 32 | 33 | minimal_tree_specific_reason = explainer.minimal_tree_specific_reason(time_limit=20) 34 | print("len minimal tree_specific_reason:", len(minimal_tree_specific_reason)) 35 | 36 | def get_pixel_value(instance, x, y, shape): 37 | index = x * shape[0] + y 38 | return instance[index] 39 | 40 | def instance_index_to_pixel_position(i, shape): 41 | return i // shape[0], i % shape[0] 42 | 43 | explainer.visualisation.screen(instance, minimal_tree_specific_reason, 44 | image={"shape": (28,28), 45 | "dtype": numpy.uint8, 46 | "get_pixel_value": get_pixel_value, 47 | "instance_index_to_pixel_position": instance_index_to_pixel_position}) 48 | -------------------------------------------------------------------------------- /pyxai/examples/BT/regression/builder-regression.py: -------------------------------------------------------------------------------- 1 | # Simple regression tree based on paper Computing Abductive Explanations for Boosted Regression Trees. 2 | # Check V1.0: Ok 3 | from pyxai import Builder, Explainer 4 | 5 | node1_1 = Builder.DecisionNode(1, operator=Builder.GT, threshold=3000, left=1500, right=1750) 6 | node1_2 = Builder.DecisionNode(1, operator=Builder.GT, threshold=2000, left=1000, right=node1_1) 7 | node1_3 = Builder.DecisionNode(1, operator=Builder.GT, threshold=1000, left=0, right=node1_2) 8 | tree1 = Builder.DecisionTree(5, node1_3) 9 | 10 | 11 | node2_1 = Builder.DecisionNode(5, operator=Builder.EQ, threshold=1, left=100, right=250) 12 | node2_2 = Builder.DecisionNode(4, operator=Builder.EQ, threshold=1, left=-100, right=node2_1) 13 | node2_3 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=1, left=node2_2, right=250) 14 | tree2 = Builder.DecisionTree(5, node2_3) 15 | 16 | node3_1 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=1, left=500, right=250) 17 | node3_2 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=1, left=250, right=100) 18 | node3_3 = Builder.DecisionNode(1, operator=Builder.GE, threshold=2000, left=0, right=node3_1) 19 | node3_4 = Builder.DecisionNode(4, operator=Builder.EQ, threshold=1, left=node3_3, right=node3_2) 20 | tree3 = Builder.DecisionTree(5, node3_4) 21 | 22 | 23 | BTs = Builder.BoostedTreesRegression([tree1, tree2, tree3]) 24 | 25 | instance = (2200, 0, 0, 1, 1) # 2200$, self employed (one hot encoded), married 26 | print("instance:", instance) 27 | 28 | explainer = Explainer.initialize(BTs, instance) 29 | 30 | print("prediction:", explainer.predict(instance)) 31 | print("direct:", explainer.to_features(explainer.direct_reason())) 32 | 33 | explainer.set_interval(1500, 2500) 34 | 35 | tree_specific = explainer.tree_specific_reason() 36 | print("tree specific:", explainer.to_features(tree_specific)) 37 | print("is tree : ", explainer.is_tree_specific_reason(tree_specific)) 38 | 39 | print("extremum", explainer.extremum_range()) 40 | print("possible range", explainer.range_for_partial_instance(instance)) 41 | explainer.visualisation.gui() 42 | #sufficient_reason = explainer.sufficient_reason() 43 | #print("sufficient: ", sufficient_reason, explainer.to_features(sufficient_reason)) 44 | #print("is implicant:", explainer.is_implicant(sufficient_reason)) -------------------------------------------------------------------------------- /pyxai/examples/BT/regression/simple-regression.py: -------------------------------------------------------------------------------- 1 | #python3 examples/BT/regression/simple-regression.py -dataset=tests/winequality-red.csv 2 | ## Check V1.0: Ok 3 | import sys 4 | 5 | from pyxai import Learning, Explainer, Tools 6 | 7 | # Machine learning part 8 | learner = Learning.Xgboost(Tools.Options.dataset, learner_type=Learning.REGRESSION) 9 | model = learner.evaluate(method=Learning.HOLD_OUT, output=Learning.BT) 10 | instances= learner.get_instances(model=model, n=10) 11 | i = 0 12 | instance = instances[i][0] 13 | p = instances[i][1] 14 | 15 | 16 | 17 | # Explanation part 18 | print("instance", instance) 19 | 20 | explainer = Explainer.initialize(model, instance) 21 | # explainer = Explainer.initialize(model, instance=instance, features_type={"numerical": Learning.DEFAULT}) 22 | 23 | prediction = explainer.predict(instance) 24 | print("prediction: ", explainer.predict(instance)) 25 | print("learner prediction: ", p) 26 | extremum_range = explainer.extremum_range() 27 | Lf = extremum_range[1] - extremum_range[0] 28 | print("extremum range: ",extremum_range) 29 | 30 | direct_reason = explainer.direct_reason() 31 | print("direct: ", explainer.to_features(direct_reason)) 32 | print("len direct: ", len(direct_reason), len(explainer.to_features(direct_reason))) 33 | #print("is a reason (for 50 checks):", explainer.is_reason(direct_reason)) 34 | 35 | print("extremum", explainer.extremum_range()) 36 | test = [None for _ in instance] 37 | print("possible range", explainer.range_for_partial_instance(instance)) 38 | sys.exit(1) 39 | percent = 2.5 40 | 41 | delta = (percent/100)*Lf 42 | print("delta=", delta) 43 | explainer.set_interval(prediction - delta, prediction + delta) 44 | print(f"set interval to : [", explainer.lower_bound, explainer.upper_bound, "]") 45 | tree_specific_reason = explainer.tree_specific_reason(n_iterations=1) 46 | print("\nlen tree_specific: ", len(tree_specific_reason), len(explainer.to_features(tree_specific_reason, eliminate_redundant_features=True))) 47 | print("\ntree_specific: ", explainer.to_features(tree_specific_reason, eliminate_redundant_features=True)) 48 | print("is a tree specific", explainer.is_tree_specific_reason(tree_specific_reason)) 49 | 50 | 51 | 52 | 53 | print("\n\nActivate theorie") 54 | explainer = Explainer.initialize(model, instance=instance, features_type={"numerical": Learning.DEFAULT}) 55 | direct_reason = explainer.direct_reason() 56 | print("len direct: ", len(direct_reason)) 57 | #print("is a reason (for 50 checks):", explainer.is_reason(direct_reason)) 58 | explainer.set_interval(prediction - delta, prediction + delta) 59 | print(f"set interval to [{1-percent} * prediction, {1+percent} * prediction]: [", explainer.lower_bound, explainer.upper_bound, "]") 60 | # 61 | tree_specific_reason = explainer.tree_specific_reason(n_iterations=1000) 62 | print("\ntree_specific: ", tree_specific_reason) 63 | print("\nlen tree_specific: ", len(tree_specific_reason)) 64 | print("\ntree_specific: ", explainer.to_features(tree_specific_reason, eliminate_redundant_features=True)) 65 | print("is a tree specific", explainer.is_tree_specific_reason(tree_specific_reason)) 66 | 67 | explainer.visualisation.gui() -------------------------------------------------------------------------------- /pyxai/examples/BT/simple.py: -------------------------------------------------------------------------------- 1 | from pyxai import Learning, Explainer, Tools 2 | 3 | # Check V1.0: Ok 4 | 5 | # Machine learning part 6 | learner = Learning.Xgboost(Tools.Options.dataset, learner_type=Learning.CLASSIFICATION) 7 | model = learner.evaluate(method=Learning.HOLD_OUT, output=Learning.BT) 8 | instance, prediction = learner.get_instances(model=model, n=1, correct=False) 9 | 10 | # Explanation part 11 | print(instance) 12 | explainer = Explainer.initialize(model, instance, features_type={"numerical": Learning.DEFAULT}) 13 | direct_reason = explainer.direct_reason() 14 | print("len direct: ", len(direct_reason)) 15 | print("is a reason (for 50 checks):", explainer.is_reason(direct_reason, n_samples=50)) 16 | 17 | 18 | tree_specific_reason = explainer.tree_specific_reason(n_iterations=10) 19 | print("\nlen tree_specific: ", len(tree_specific_reason)) 20 | print("\ntree_specific: ", explainer.to_features(tree_specific_reason, eliminate_redundant_features=True)) 21 | 22 | tree_specific_reason = explainer.tree_specific_reason(n_iterations=10, weights=[1,1,1,1,1,1,1,1,1,1,11]) 23 | print("\nlen tree_specific: ", len(tree_specific_reason)) 24 | print("\ntree_specific: ", explainer.to_features(tree_specific_reason, eliminate_redundant_features=True)) 25 | 26 | 27 | explainer.set_excluded_features(["score_factor"]) 28 | contrastive_reason = explainer.minimal_contrastive_reason(n=1) 29 | print("\n\ncontrastive reason: ", explainer.to_features(contrastive_reason, contrastive=True)) 30 | print("is contrastive: ", explainer.is_contrastive_reason(contrastive_reason)) 31 | print("elapsed time: ", explainer.elapsed_time) 32 | print() 33 | #instances = learner.get_instances(n=100) 34 | 35 | #print(instances) 36 | #for inst, p in instances: 37 | # explainer.set_instance(inst) 38 | # direct_reason = explainer.direct_reason() 39 | # 40 | # tree_specific_reason = explainer.tree_specific_reason(n_iterations=100) 41 | # print("is a tree specific", explainer.is_tree_specific_reason(tree_specific_reason)) 42 | 43 | #minimal_tree_specific_reason = explainer.minimal_tree_specific_reason(time_limit=20) 44 | #print("\nlen minimal tree_specific: ", len(minimal_tree_specific_reason)) 45 | #print("is a tree specific", explainer.is_tree_specific_reason(minimal_tree_specific_reason)) 46 | #if explainer.elapsed_time == Explainer.TIMEOUT: print("Not minimal, this is an approximation") 47 | # s = explainer.sufficient_reason() 48 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/bad/converter-dorothea.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://www.openml.org/search?type=data&sort=runs&id=4137&status=active 2 | 3 | # Very long to convert. 4 | 5 | from pyxai import Learning, Explainer, Tools 6 | 7 | converter = Learning.Converter(Tools.Options.dataset, target_feature="100000", classification_type=Learning.BINARY_CLASS) # class Converter 8 | 9 | converter.all_numerical_features() 10 | 11 | converter.process() 12 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 13 | converter.export(dataset_name, output="examples/datasets_converted") 14 | 15 | 16 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-adult.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://archive.ics.uci.edu/ml/datasets/adult 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | import datetime 6 | import pandas 7 | data = pandas.read_csv(Tools.Options.dataset, names=['age', 'workclass', 'fnlwgt', 'education', 'education-num','marital-status', 'occupation', 'relationship','race', 'sex', 'capital-gain','capital-loss', 'hours-per-week', 'native-country', 'salary'], skiprows=1) 8 | preprocessor = Learning.Preprocessor(data, target_feature="salary", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 9 | 10 | print("data:", preprocessor.data) 11 | 12 | preprocessor.unset_features(["education-num"]) 13 | 14 | preprocessor.set_categorical_features(columns=["workclass", "education", "marital-status", "occupation", "relationship", "race", "sex", "native-country"]) 15 | 16 | preprocessor.set_numerical_features({"age": None, "fnlwgt": None, "capital-gain": None, "capital-loss": None, "hours-per-week": None}) 17 | 18 | preprocessor.process() 19 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 20 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 21 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-arrowhead.py: -------------------------------------------------------------------------------- 1 | # dataset source:http://www.timeseriesclassification.com/description.php?Dataset=ArrowHead 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | import pandas 6 | 7 | data = pandas.read_csv(Tools.Options.dataset, names=["V"+str(i) for i in range(1,250)]+["label"], sep=',') 8 | 9 | preprocessor = Learning.Preprocessor(data, target_feature="label", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 10 | 11 | 12 | preprocessor.all_numerical_features() 13 | 14 | preprocessor.process() 15 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 16 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 17 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-australian.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://archive.ics.uci.edu/ml/datasets/adult 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | import pandas 6 | data = pandas.read_csv(Tools.Options.dataset, names=["A"+str(i) for i in range(1,16)], sep=',') 7 | 8 | preprocessor = Learning.Preprocessor(data, target_feature="A15", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 9 | 10 | print("data:", preprocessor.data) 11 | 12 | preprocessor.set_categorical_features(columns=["A1", "A4", "A5", "A6", "A8", "A9", "A11", "A12"]) 13 | 14 | 15 | #datetime.date(d.split("/")[2], d.split("/")[1], d.split("/")[0]).toordinal() 16 | preprocessor.set_numerical_features({ 17 | "A2": None, 18 | "A3": None, 19 | "A7": None, 20 | "A10": None, 21 | "A13": None, 22 | "A14": None, 23 | }) 24 | 25 | preprocessor.process() 26 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 27 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 28 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-balance-scale.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://archive.ics.uci.edu/ml/datasets/balance+scale 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | import datetime 6 | import pandas 7 | data = pandas.read_csv(Tools.Options.dataset, names=['Class', 'Left-Weight', 'Left-Distance', 'Right-Weight', 'Right-Distance']) 8 | preprocessor = Learning.Preprocessor(data, target_feature="Class", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 9 | 10 | print("data:", preprocessor.data) 11 | 12 | preprocessor.all_numerical_features() 13 | 14 | preprocessor.process() 15 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 16 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 17 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-bank.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://archive.ics.uci.edu/ml/datasets/adult 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | import pandas 6 | data = pandas.read_csv(Tools.Options.dataset, names=["age","job","marital","education","default","balance","housing","loan","contact","day","month","duration","campaign","pdays","previous","poutcome","y"], skiprows=1, sep=";") 7 | 8 | preprocessor = Learning.Preprocessor(data, target_feature="y", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 9 | 10 | print("data:", preprocessor.data) 11 | 12 | preprocessor.unset_features([]) 13 | 14 | preprocessor.set_categorical_features(columns=["job", "marital", "education", "default", "housing", "loan", "contact", "month", "poutcome"]) 15 | 16 | preprocessor.set_numerical_features({ 17 | "age": None, 18 | "balance": None, 19 | "day": None, 20 | "duration": None, 21 | "campaign": None, 22 | "pdays": None, 23 | "previous": None 24 | }) 25 | 26 | preprocessor.process() 27 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 28 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 29 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-biodegradation.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://www.openml.org/search?type=data&status=active&id=1494&sort=runs 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | import datetime 6 | 7 | 8 | import pandas 9 | 10 | preprocessor = Learning.Preprocessor(Tools.Options.dataset, target_feature="41", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 11 | 12 | print("data:", preprocessor.data) 13 | 14 | preprocessor.all_numerical_features() 15 | 16 | preprocessor.process() 17 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 18 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 19 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-breast-tumor.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://www.openml.org/search?type=data&status=active&id=844&sort=runs 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | import pandas 6 | 7 | 8 | 9 | data = pandas.read_csv(Tools.Options.dataset, names=["age","menopause","inv-nodes","node-caps","deg-malig","breast","breast-quad","irradiation","recurrence","binaryClass"], sep=',') 10 | preprocessor = Learning.Preprocessor(data, target_feature="binaryClass", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 11 | 12 | print("data:", preprocessor.data) 13 | 14 | 15 | preprocessor.set_categorical_features(columns=["menopause", "inv-nodes", "node-caps", "deg-malig", "breast", "breast-quad","irradiation", "recurrence"]) 16 | 17 | #datetime.date(d.split("/")[2], d.split("/")[1], d.split("/")[0]).toordinal() 18 | preprocessor.set_numerical_features({ "age": None }) 19 | 20 | preprocessor.process() 21 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 22 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 23 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-bupa.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://archive.ics.uci.edu/ml/datasets/adult 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | import datetime 6 | import pandas 7 | data = pandas.read_csv(Tools.Options.dataset, names=['mcv', 'alkphos', 'sgpt', 'sgot', 'gammagt','drinks', 'selector']) 8 | preprocessor = Learning.Preprocessor(data, target_feature="drinks", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 9 | 10 | print("data:", preprocessor.data) 11 | 12 | preprocessor.unset_features(["selector"]) 13 | 14 | preprocessor.all_numerical_features() 15 | 16 | preprocessor.process() 17 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 18 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 19 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-christine.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://www.openml.org/search?type=data&status=active&id=41142&sort=runs 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | preprocessor = Learning.Preprocessor(Tools.Options.dataset, target_feature="1636", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 6 | 7 | 8 | preprocessor.all_numerical_features() 9 | 10 | preprocessor.process() 11 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 12 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 13 | 14 | 15 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-cifar.py: -------------------------------------------------------------------------------- 1 | # dataset source: http://www.cs.toronto.edu/~kriz/cifar.html (python version) 2 | 3 | import pickle 4 | import os 5 | import pandas 6 | import numpy 7 | 8 | path="../../cifar-10-batches-py/" 9 | 10 | def unpickle(file): 11 | with open(file, 'rb') as fo: 12 | dict = pickle.load(fo, encoding='bytes') 13 | return dict 14 | 15 | def dataset_to_dict(): 16 | for root, dirs, files in os.walk(path): 17 | for name in files: 18 | if name == "data_batch_1": 19 | return unpickle(root + name) 20 | 21 | dataset = dataset_to_dict() 22 | 23 | keys_cats = [k for k in range(len(dataset[b'labels'])) if dataset[b'labels'][k] == 3] 24 | keys_dogs = [k for k in range(len(dataset[b'labels'])) if dataset[b'labels'][k] == 5] 25 | 26 | rawdata = [] 27 | for key in keys_cats: 28 | rawdata.append(numpy.append(dataset[b'data'][key], [dataset[b'labels'][key]])) 29 | for key in keys_dogs: 30 | rawdata.append(numpy.append(dataset[b'data'][key], [dataset[b'labels'][key]])) 31 | 32 | dataframe = pandas.DataFrame(rawdata) 33 | dataframe.columns = [str(i) for i in range(len(dataset[b'data'][key]))]+["label"] 34 | dataframe.to_csv("examples/datasets_not_converted/cifar_cat_dog.csv", index=False) 35 | print(dataframe) -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-cleveland.py: -------------------------------------------------------------------------------- 1 | # https://www.openml.org/search?type=data&status=active&id=786&sort=runs 2 | from pyxai import Learning, Explainer, Tools 3 | 4 | import datetime 5 | 6 | preprocessor = Learning.Preprocessor(Tools.Options.dataset, target_feature="target", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 7 | 8 | print("data:", preprocessor.data) 9 | 10 | preprocessor.set_categorical_features(columns=["sex", "cp", "fbs", "restecg", "exang", "slope", "thal"]) 11 | 12 | #datetime.date(d.split("/")[2], d.split("/")[1], d.split("/")[0]).toordinal() 13 | preprocessor.set_numerical_features({ 14 | "age": None, 15 | "trestbps": None, 16 | "chol": None, 17 | "thalach": None, 18 | "oldpeak": None, 19 | "ca": None, 20 | }) 21 | 22 | preprocessor.process() 23 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 24 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 25 | 26 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-cnae.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://archive.ics.uci.edu/ml/datasets/cnae-9 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | import datetime 6 | import pandas 7 | data = pandas.read_csv(Tools.Options.dataset, names=['category']+["W"+str(i) for i in range(856)], skiprows=1) 8 | preprocessor = Learning.Preprocessor(data, target_feature="category", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 9 | 10 | print("data:", preprocessor.data) 11 | 12 | preprocessor.all_numerical_features() 13 | 14 | preprocessor.process() 15 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 16 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 17 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-compas.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://www.kaggle.com/datasets/danofer/compass 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | preprocessor = Learning.Preprocessor(Tools.Options.dataset, target_feature="Two_yr_Recidivism", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 6 | 7 | 8 | print("data:", preprocessor.data) 9 | 10 | preprocessor.set_categorical_features_already_one_hot_encoded("score_factor", ["score_factor"]) 11 | preprocessor.set_categorical_features_already_one_hot_encoded("Age_Above_FourtyFive", ["Age_Above_FourtyFive"]) 12 | preprocessor.set_categorical_features_already_one_hot_encoded("Age_Below_TwentyFive", ["Age_Below_TwentyFive"]) 13 | preprocessor.set_categorical_features_already_one_hot_encoded("Ethnic", ["African_American", "Asian", "Hispanic", "Native_American", "Other"]) 14 | preprocessor.set_categorical_features_already_one_hot_encoded("Female", ["Female"]) 15 | preprocessor.set_categorical_features_already_one_hot_encoded("Misdemeanor", ["Misdemeanor"]) 16 | 17 | preprocessor.set_numerical_features({ 18 | "Number_of_Priors": None 19 | }) 20 | 21 | preprocessor.process() 22 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 23 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 24 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-contraceptive.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://archive.ics.uci.edu/ml/datasets/Contraceptive+Method+Choice 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | import datetime 6 | import pandas 7 | data = pandas.read_csv(Tools.Options.dataset, names=['age', 'wife-education', 'husband-education', 'children', 'religion','job', 'occupation', 'index','media', 'contraceptive_method'], skiprows=1) 8 | 9 | preprocessor = Learning.Preprocessor(data, target_feature="contraceptive_method", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 10 | 11 | print("data:", preprocessor.data) 12 | 13 | 14 | preprocessor.set_categorical_features(columns=["wife-education","husband-education","religion","job","occupation","index","media"]) 15 | 16 | preprocessor.set_numerical_features({ "age": None, "children": None }) 17 | 18 | preprocessor.process() 19 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 20 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 21 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-default-paiement.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://archive.ics.uci.edu/ml/datasets/adult 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | import datetime 6 | 7 | preprocessor = Learning.Preprocessor(Tools.Options.dataset, target_feature="default.payment.next.month", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 8 | 9 | preprocessor.unset_features(["ID"]) 10 | 11 | #,LIMIT_BAL,SEX,EDUCATION,MARRIAGE,AGE,PAY_0,PAY_2,PAY_3,PAY_4,PAY_5,PAY_6,BILL_AMT1,BILL_AMT2,BILL_AMT3,BILL_AMT4,BILL_AMT5,BILL_AMT6,PAY_AMT1,PAY_AMT2,PAY_AMT3,PAY_AMT4,PAY_AMT5,PAY_AMT6,default.payment.next.month 12 | preprocessor.set_categorical_features(columns=["SEX", "EDUCATION", "MARRIAGE", "PAY_0", "PAY_2", "PAY_3", "PAY_4","PAY_5", "PAY_6"]) 13 | 14 | preprocessor.set_numerical_features({ 15 | "LIMIT_BAL": None, 16 | "AGE": None, 17 | "BILL_AMT1": None, 18 | "BILL_AMT2": None, 19 | "BILL_AMT3": None, 20 | "BILL_AMT4": None, 21 | "BILL_AMT5": None, 22 | "BILL_AMT6": None, 23 | "PAY_AMT1": None, 24 | "PAY_AMT2": None, 25 | "PAY_AMT3": None, 26 | "PAY_AMT4": None, 27 | "PAY_AMT5": None, 28 | "PAY_AMT6": None 29 | }) 30 | 31 | preprocessor.process() 32 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 33 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 34 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-dexter.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://www.openml.org/search?type=data&sort=runs&id=4136&status=active 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | preprocessor = Learning.Preprocessor(Tools.Options.dataset, target_feature="20000", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 6 | 7 | preprocessor.all_numerical_features() 8 | 9 | preprocessor.process() 10 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 11 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 12 | 13 | 14 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-divorce.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://archive.ics.uci.edu/ml/datasets/Divorce+Predictors+data+set 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | import datetime 6 | import pandas 7 | data = pandas.read_csv(Tools.Options.dataset, sep=';') 8 | 9 | preprocessor = Learning.Preprocessor(data, target_feature="Divorce", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 10 | print("data:", preprocessor.data) 11 | 12 | preprocessor.all_numerical_features() 13 | 14 | preprocessor.process() 15 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 16 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 17 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-german.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://archive.ics.uci.edu/ml/datasets/statlog+(german+credit+data) 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | import datetime 6 | import pandas 7 | 8 | data = pandas.read_csv(Tools.Options.dataset, names=["V"+str(i) for i in range(1, 22)], sep='\s+') #Warning, here there are no index columns 9 | print("names:", ["V"+str(i) for i in range(1, 22)]) 10 | print("data:", data) 11 | 12 | preprocessor = Learning.Preprocessor(data, target_feature="V21", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 13 | preprocessor.set_categorical_features(columns=["V1", "V3", "V4", "V6", "V7", "V9", "V10", "V12", "V14", "V15", "V17", "V19", "V20"]) 14 | 15 | preprocessor.set_numerical_features({ 16 | "V2": None, 17 | "V5": None, 18 | "V8": None, 19 | "V11": None, 20 | "V13": None, 21 | "V16": None, 22 | "V18": None, 23 | }) 24 | 25 | preprocessor.process() 26 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 27 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 28 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-gisette.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://archive.ics.uci.edu/ml/datasets/adult 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | import datetime 6 | 7 | 8 | import pandas 9 | 10 | preprocessor = Learning.Preprocessor(Tools.Options.dataset, target_feature="5000", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 11 | print("data:", preprocessor.data) 12 | 13 | preprocessor.all_numerical_features() 14 | 15 | preprocessor.process() 16 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 17 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 18 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-melb.py: -------------------------------------------------------------------------------- 1 | # usage 2 | # python3 examples/Converters/converter-melb.py -dataset=../../melb_data.csv 3 | 4 | from pyxai import Learning, Explainer, Tools 5 | 6 | import datetime 7 | 8 | # Machine learning part 9 | # NUMERICAL: with an order (Ordinal Encoding) 10 | # CATEGORICAL: without an order 11 | 12 | preprocessor = Learning.Preprocessor(Tools.Options.dataset, target_feature="Type", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS, to_binary_classification=Learning.ONE_VS_REST) 13 | 14 | preprocessor.unset_features(["Address", "Suburb", "SellerG"]) 15 | 16 | preprocessor.set_categorical_features(columns=["Method", "CouncilArea", "Regionname"]) 17 | 18 | #datetime.date(d.split("/")[2], d.split("/")[1], d.split("/")[0]).toordinal() 19 | preprocessor.set_numerical_features({ 20 | "Postcode": lambda d: int(d), 21 | "Rooms": None, 22 | "Price": None, 23 | "Date": lambda d: datetime.date(int(d.split("/")[2]), int(d.split("/")[1]), int(d.split("/")[0])).toordinal(), 24 | "Distance": None, 25 | "Bedroom2": None, 26 | "Bathroom": None, 27 | "Car": None, 28 | "Landsize": None, 29 | "BuildingArea": None, 30 | "YearBuilt": None, 31 | "Lattitude": None, 32 | "Longtitude": None, 33 | "Propertycount": None 34 | }) 35 | 36 | preprocessor.process() 37 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 38 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 39 | 40 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-mnist.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://archive.ics.uci.edu/ml/datasets/adult 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | import datetime 6 | 7 | 8 | import pandas 9 | 10 | preprocessor = Learning.Preprocessor(Tools.Options.dataset, target_feature="784", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 11 | print("data:", preprocessor.data) 12 | 13 | preprocessor.all_numerical_features() 14 | 15 | preprocessor.process() 16 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 17 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 18 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-nerve.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://www.timeseriesclassification.com/description.php?Dataset=NerveDamage 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | import pandas 6 | 7 | data = pandas.read_csv(Tools.Options.dataset, names=["V"+str(i) for i in range(1,1501)]+["label"], sep="\s+|,|:") 8 | 9 | preprocessor = Learning.Preprocessor(data, target_feature="label", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 10 | 11 | 12 | preprocessor.all_numerical_features() 13 | 14 | preprocessor.process() 15 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 16 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 17 | 18 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-regression.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://www.kaggle.com/datasets/uciml/red-wine-quality-cortez-et-al-2009 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | import datetime 6 | import pandas 7 | data = pandas.read_csv(Tools.Options.dataset) 8 | preprocessor = Learning.Preprocessor(data, target_feature="quality", learner_type=Learning.REGRESSION) 9 | 10 | print("data:", preprocessor.data) 11 | 12 | preprocessor.all_numerical_features() 13 | 14 | preprocessor.process() 15 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 16 | preprocessor.export(dataset_name, output_directory="examples/datasets_converted") 17 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-spambase.py: -------------------------------------------------------------------------------- 1 | # dataset source: https://archive.ics.uci.edu/ml/datasets/spambase 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | import datetime 6 | 7 | 8 | import pandas 9 | data = pandas.read_csv(Tools.Options.dataset, names=["V"+str(i) for i in range(58)]) 10 | 11 | preprocessor = Learning.Preprocessor(data, target_feature="V57", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 12 | print("data:", preprocessor.data) 13 | preprocessor.all_numerical_features() 14 | 15 | preprocessor.process() 16 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 17 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 18 | -------------------------------------------------------------------------------- /pyxai/examples/Converters/converter-wine.py: -------------------------------------------------------------------------------- 1 | # dataset source: http://www.timeseriesclassification.com/description.php?Dataset=Wine 2 | 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | import pandas 6 | 7 | data = pandas.read_csv(Tools.Options.dataset, names=["V"+str(i) for i in range(1,235)]+["label"], sep=',') 8 | 9 | preprocessor = Learning.Preprocessor(data, target_feature="label", learner_type=Learning.CLASSIFICATION, classification_type=Learning.BINARY_CLASS) 10 | 11 | 12 | preprocessor.all_numerical_features() 13 | 14 | preprocessor.process() 15 | dataset_name = Tools.Options.dataset.split("/")[-1].split(".")[0] 16 | preprocessor.export(dataset_name, output_directory=Tools.Options.output) 17 | 18 | -------------------------------------------------------------------------------- /pyxai/examples/DT/GUI-cifar.py: -------------------------------------------------------------------------------- 1 | from pyxai import Learning, Explainer, Tools 2 | 3 | # To use with the minist dataset for example 4 | # (classification between 4 and 9 or between 3 and 8) 5 | # available here: 6 | # http://www.cril.univ-artois.fr/expekctation/datasets/mnist38.csv 7 | # http://www.cril.univ-artois.fr/expekctation/datasets/mnist49.csv 8 | # Check V1.0: Ok 9 | import numpy 10 | 11 | # Dataset 12 | dataset = "./examples/datasets_not_converted/cifar_cat_dog.csv" 13 | 14 | # Tuning 15 | def tuning(): 16 | import pandas 17 | from sklearn.model_selection import GridSearchCV 18 | from sklearn.ensemble import GradientBoostingClassifier, RandomForestClassifier 19 | 20 | def load_dataset(dataset): 21 | data = pandas.read_csv(dataset).copy() 22 | 23 | # extract labels 24 | labels = data[data.columns[-1]] 25 | labels = numpy.array(labels) 26 | 27 | # remove the label of each instance 28 | data = data.drop(columns=[data.columns[-1]]) 29 | 30 | # extract the feature names 31 | feature_names = list(data.columns) 32 | 33 | return data.values, labels, feature_names 34 | 35 | X, Y, names = load_dataset(dataset) 36 | model1 = RandomForestClassifier() 37 | param_grid = {'n_estimators': [100, 250, 500], 'max_depth': [5, 8, 12], 'max_features' : [5, 10, 16] } 38 | gridsearch1 = GridSearchCV(model1, 39 | param_grid=param_grid, 40 | scoring='balanced_accuracy', refit=True, cv=3, 41 | return_train_score=True, verbose=10) 42 | 43 | gridsearch1.fit(X, Y) 44 | return gridsearch1.best_params_ 45 | best_parameters = tuning() 46 | print("Best parameters from tuning:", best_parameters) 47 | 48 | # Machine learning part 49 | learner = Learning.Scikitlearn(dataset, learner_type=Learning.CLASSIFICATION) 50 | model = learner.evaluate(method=Learning.HOLD_OUT, output=Learning.RF, **best_parameters) 51 | instances = learner.get_instances(model, n=10, correct=True) 52 | 53 | explainer = Explainer.initialize(model) 54 | 55 | for (instance, prediction) in instances: 56 | explainer.set_instance(instance) 57 | sufficient = explainer.sufficient_reason() 58 | print("suffisante done") 59 | minimals = explainer.minimal_majoritary_reason(n=1, time_limit=60) 60 | print("minimal done") 61 | 62 | #Get from the position of a pixel 'x' and 'y' a color value (r, g, b) according to an 'instance' 63 | 64 | def get_pixel_value(instance, x, y, shape): 65 | n_pixels = shape[0]*shape[1] 66 | index = x * shape[0] + y 67 | return (instance[0:n_pixels][index], instance[n_pixels:n_pixels*2][index],instance[n_pixels*2:][index]) 68 | 69 | def instance_index_to_pixel_position(i, shape): 70 | n_pixels = shape[0]*shape[1] 71 | if i < n_pixels: 72 | value = i 73 | elif i >= n_pixels and i < n_pixels*2: 74 | value = i - n_pixels 75 | else: 76 | value = i - (n_pixels*2) 77 | return value // shape[0], value % shape[0] 78 | 79 | explainer.visualisation.gui(image={"shape": (32,32,3), 80 | "dtype": numpy.uint8, 81 | "get_pixel_value": get_pixel_value, 82 | "instance_index_to_pixel_position": instance_index_to_pixel_position}) 83 | -------------------------------------------------------------------------------- /pyxai/examples/DT/GUI-mnist38-contrastives.py: -------------------------------------------------------------------------------- 1 | from pyxai import Learning, Explainer, Tools 2 | 3 | # To use with the minist dataset for example 4 | # (classification between 4 and 9 or between 3 and 8) 5 | # available here: 6 | # http://www.cril.univ-artois.fr/expekctation/datasets/mnist38.csv 7 | # http://www.cril.univ-artois.fr/expekctation/datasets/mnist49.csv 8 | # Check V1.0: Ok 9 | 10 | # the location of the dataset 11 | import numpy 12 | dataset = "./examples/datasets_not_converted/mnist38.csv" 13 | 14 | # Machine learning part 15 | learner = Learning.Scikitlearn(dataset, learner_type=Learning.CLASSIFICATION) 16 | model = learner.evaluate(method=Learning.HOLD_OUT, output=Learning.DT) 17 | instances = learner.get_instances(model, n=10, correct=False) 18 | 19 | # Explanation part 20 | explainer = Explainer.initialize(model) 21 | 22 | for (instance, prediction) in instances: 23 | explainer.set_instance(instance) 24 | contrastive_reasons = explainer.contrastive_reason(n=Explainer.ALL) 25 | 26 | # Visualisation part 27 | explainer.heat_map("heat map 1", contrastive_reasons, contrastive=True) 28 | 29 | def get_pixel_value(instance, x, y, shape): 30 | index = x * shape[0] + y 31 | return instance[index] 32 | 33 | def instance_index_to_pixel_position(i, shape): 34 | return i // shape[0], i % shape[0] 35 | 36 | explainer.visualisation.gui(image={"shape": (28,28), 37 | "dtype": numpy.uint8, 38 | "get_pixel_value": get_pixel_value, 39 | "instance_index_to_pixel_position": instance_index_to_pixel_position}) 40 | 41 | -------------------------------------------------------------------------------- /pyxai/examples/DT/GUI-mnist38.py: -------------------------------------------------------------------------------- 1 | from pyxai import Learning, Explainer, Tools 2 | import numpy 3 | # To use with the minist dataset for example 4 | # (classification between 4 and 9 or between 3 and 8) 5 | # available here: 6 | # http://www.cril.univ-artois.fr/expekctation/datasets/mnist38.csv 7 | # http://www.cril.univ-artois.fr/expekctation/datasets/mnist49.csv 8 | # Check V1.0: Ok 9 | 10 | # the location of the dataset 11 | dataset = "./examples/datasets_not_converted/mnist38.csv" 12 | 13 | # Machine learning part 14 | learner = Learning.Scikitlearn(dataset, learner_type=Learning.CLASSIFICATION) 15 | model = learner.evaluate(method=Learning.HOLD_OUT, output=Learning.DT) 16 | instance, prediction = learner.get_instances(model, n=1, correct=True) 17 | 18 | print("instance:", instance) 19 | print("prediction:", prediction) 20 | 21 | # Explanation part 22 | explainer = Explainer.initialize(model, instance) 23 | direct = explainer.direct_reason() 24 | print("direct:", direct) 25 | 26 | sufficient_reasons = explainer.sufficient_reason(n=100, time_limit=100) # take 100 in order to have different reasons 27 | assert explainer.is_sufficient_reason(sufficient_reasons[-1]), "This is not a sufficient reason !" 28 | 29 | minimal = explainer.minimal_sufficient_reason() 30 | print("minimal:", minimal) 31 | assert explainer.is_sufficient_reason(minimal), "This is not a sufficient reason !" 32 | 33 | sufficient_reasons_per_attribute = explainer.n_sufficient_reasons_per_attribute() 34 | print("\nsufficient_reasons_per_attribute:", sufficient_reasons_per_attribute) 35 | print("\nnumber of sufficient reasons:", explainer.n_sufficient_reasons()) 36 | 37 | def get_pixel_value(instance, x, y, shape): 38 | index = x * shape[0] + y 39 | return instance[index] 40 | 41 | def instance_index_to_pixel_position(i, shape): 42 | return i // shape[0], i % shape[0] 43 | 44 | # Visualization part 45 | explainer.visualisation.heat_map("heat map 1", sufficient_reasons_per_attribute) 46 | explainer.visualisation.gui(image={"shape": (28,28), 47 | "dtype": numpy.uint8, 48 | "get_pixel_value": get_pixel_value, 49 | "instance_index_to_pixel_position": instance_index_to_pixel_position}) 50 | -------------------------------------------------------------------------------- /pyxai/examples/DT/base.py: -------------------------------------------------------------------------------- 1 | from pyxai import Learning, Explainer 2 | 3 | learner = Learning.Scikitlearn("pyxai/tests/dermatology.csv", learner_type=Learning.CLASSIFICATION) 4 | model = learner.evaluate(method=Learning.HOLD_OUT, output=Learning.DT) 5 | instance, prediction = learner.get_instances(model, n=1, correct=True, predictions=[0]) 6 | 7 | print("le:", learner.dict_labels) 8 | 9 | explainer = Explainer.initialize(model, instance) 10 | print("instance:", instance) 11 | print("binary representation:", explainer.binary_representation) 12 | 13 | sufficient_reason = explainer.sufficient_reason(n=1) 14 | print("sufficient_reason:", sufficient_reason) 15 | print("to_features:", explainer.to_features(sufficient_reason)) 16 | 17 | instance, prediction = learner.get_instances(model, n=1, correct=False) 18 | explainer.set_instance(instance) 19 | contrastive_reason = explainer.contrastive_reason() 20 | print("contrastive reason", contrastive_reason) 21 | print("to_features:", explainer.to_features(contrastive_reason, contrastive=True)) 22 | 23 | explainer.visualisation.screen(instance, contrastive_reason, contrastive=True) -------------------------------------------------------------------------------- /pyxai/examples/DT/builder-loan.py: -------------------------------------------------------------------------------- 1 | # Example: 2 | # Instances are described by three attributes: A (numerical), B_1 and B_2 (boolean). 3 | # The value of A gives the annual income of a customer (in k$) 4 | # B_1 indicates whether the customer has no debts, and B_2 indicates that the customer has reimbursed his/her previous loan. 5 | # This is the classifier representing an approximation of the exact function. 6 | # The exact function, which we know, is: the loan is granted if and only if the client's annual income is at least $25k or if the client has no debts and has paid off his previous loan: 7 | # f(x) = 1 <=> (v1 >= 25) and ((v2 == 1) or (v3 == 1)). 8 | # @article{TODO} 9 | # Check V1.0: Ok 10 | 11 | from pyxai import Builder, Explainer 12 | 13 | # Builder part 14 | 15 | node_v3_1 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=1, left=0, right=1) 16 | node_v2_1 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=1, left=0, right=node_v3_1) 17 | 18 | node_v3_2 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=1, left=0, right=1) 19 | node_v2_2 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=1, left=0, right=node_v3_2) 20 | 21 | node_v3_3 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=1, left=0, right=1) 22 | node_v2_3 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=1, left=0, right=node_v3_3) 23 | 24 | node_v1_1 = Builder.DecisionNode(1, operator=Builder.GE, threshold=10, left=node_v2_1, right=node_v2_2) 25 | node_v1_2 = Builder.DecisionNode(1, operator=Builder.GE, threshold=20, left=node_v1_1, right=node_v2_3) 26 | node_v1_3 = Builder.DecisionNode(1, operator=Builder.GE, threshold=30, left=node_v1_2, right=1) 27 | node_v1_4 = Builder.DecisionNode(1, operator=Builder.GE, threshold=40, left=node_v1_3, right=1) 28 | 29 | tree = Builder.DecisionTree(3, node_v1_4) 30 | 31 | loan_types = { 32 | "numerical": ["f1"], 33 | "binary": ["f2", "f3"], 34 | } 35 | 36 | print("bob = (20, 1, 0):") 37 | bob = (20, 1, 0) 38 | explainer = Explainer.initialize(tree, instance=bob, features_type=loan_types) 39 | 40 | print("binary representation: ", explainer.binary_representation) 41 | print("target_prediction:", explainer.target_prediction) 42 | print("to_features:", explainer.to_features(explainer.binary_representation, eliminate_redundant_features=False)) 43 | 44 | minimals = explainer.minimal_sufficient_reason() 45 | print("Minimal sufficient reasons:", minimals) 46 | print("to_features:", explainer.to_features(minimals)) 47 | 48 | print("charles = (5, 0, 0):") 49 | charles = (5, 0, 0) 50 | explainer.set_instance(charles) 51 | 52 | print("binary representation: ", explainer.binary_representation) 53 | print("target_prediction:", explainer.target_prediction) 54 | print("to_features:", explainer.to_features(explainer.binary_representation, eliminate_redundant_features=False)) 55 | 56 | contrastives = explainer.contrastive_reason(n=Explainer.ALL) 57 | for contrastive in contrastives: 58 | print("contrastive:", explainer.to_features(contrastive, contrastive=True)) 59 | 60 | explainer.visualisation.gui() -------------------------------------------------------------------------------- /pyxai/examples/DT/builder-orchid.py: -------------------------------------------------------------------------------- 1 | # Example taken from the paper quoted below: "The decision tree in Figure 1 separates Cattleya orchids from other 2 | # orchids using the following features: x1: “has fragrant flowers”, x2: “has one or two leaves”, 3 | # x3: “has large flowers”, and x4: “is sympodial”." 4 | 5 | # @article{AUDEMARD2022102088, 6 | # title = {On the explanatory power of Boolean decision trees}, 7 | # journal = {Data & Knowledge Engineering}, 8 | # pages = {102088}, 9 | # year = {2022}, 10 | # issn = {0169-023X}, 11 | # doi = {https://doi.org/10.1016/j.datak.2022.102088}, 12 | # url = {https://www.sciencedirect.com/science/article/pii/S0169023X22000799}, 13 | # author = {Gilles Audemard and Steve Bellart and Louenas Bounia and Frédéric Koriche and Jean-Marie Lagniez and Pierre Marquis}, 14 | # } 15 | # Check V1.0: Ok 16 | 17 | from pyxai import Builder, Explainer 18 | 19 | # Builder part 20 | node_x4_1 = Builder.DecisionNode(4, left=0, right=1) 21 | node_x4_2 = Builder.DecisionNode(4, left=0, right=1) 22 | node_x4_3 = Builder.DecisionNode(4, left=0, right=1) 23 | node_x4_4 = Builder.DecisionNode(4, left=0, right=1) 24 | node_x4_5 = Builder.DecisionNode(4, left=0, right=1) 25 | 26 | node_x3_1 = Builder.DecisionNode(3, left=0, right=node_x4_1) 27 | node_x3_2 = Builder.DecisionNode(3, left=node_x4_2, right=node_x4_3) 28 | node_x3_3 = Builder.DecisionNode(3, left=node_x4_4, right=node_x4_5) 29 | 30 | node_x2_1 = Builder.DecisionNode(2, left=0, right=node_x3_1) 31 | node_x2_2 = Builder.DecisionNode(2, left=node_x3_2, right=node_x3_3) 32 | 33 | node_x1_1 = Builder.DecisionNode(1, left=node_x2_1, right=node_x2_2) 34 | 35 | tree = Builder.DecisionTree(4, node_x1_1, force_features_equal_to_binaries=True) 36 | 37 | # Explainer part for instance = (1,1,1,1) 38 | print("instance = (1,1,1,1):") 39 | explainer = Explainer.initialize(tree, instance=(1, 1, 1, 1)) 40 | 41 | print("target_prediction:", explainer.target_prediction) 42 | direct = explainer.direct_reason() 43 | print("direct:", direct) 44 | assert direct == (1, 2, 3, 4), "The direct reason is not good !" 45 | 46 | sufficient_reasons = explainer.sufficient_reason(n=Explainer.ALL) 47 | print("sufficient_reasons:", sufficient_reasons) 48 | assert sufficient_reasons == ((1, 4), (2, 3, 4)), "The sufficient reasons are not good !" 49 | 50 | for sufficient in sufficient_reasons: 51 | assert explainer.is_sufficient_reason(sufficient), "This is have to be a sufficient reason !" 52 | 53 | minimals = explainer.minimal_sufficient_reason() 54 | print("Minimal sufficient reasons:", minimals) 55 | assert minimals == (1, 4), "The minimal sufficient reasons are not good !" 56 | 57 | contrastives = explainer.contrastive_reason(n=Explainer.ALL) 58 | print("Contrastives:", contrastives) 59 | for contrastive in contrastives: 60 | assert explainer.is_contrastive_reason(contrastive), "This is not a contrastive reason !" 61 | 62 | # Explainer part for instance = (0,0,0,0) 63 | print("\ninstance = (0,0,0,0):") 64 | 65 | explainer.set_instance((0, 0, 0, 0)) 66 | 67 | print("target_prediction:", explainer.target_prediction) 68 | direct = explainer.direct_reason() 69 | print("direct:", direct) 70 | assert direct == (-1, -2), "The direct reason is not good !" 71 | 72 | sufficient_reasons = explainer.sufficient_reason(n=Explainer.ALL) 73 | print("sufficient_reasons:", sufficient_reasons) 74 | assert sufficient_reasons == ((-4,), (-1, -2), (-1, -3)), "The sufficient reasons are not good !" 75 | for sufficient in sufficient_reasons: 76 | assert explainer.is_sufficient_reason(sufficient), "This is not a sufficient reason !" 77 | minimals = explainer.minimal_sufficient_reason(n=1) 78 | print("Minimal sufficient reasons:", minimals) 79 | assert minimals == (-4,), "The minimal sufficient reasons are not good !" 80 | 81 | explainer.visualisation.gui() 82 | -------------------------------------------------------------------------------- /pyxai/examples/DT/builder-rectify1.py: -------------------------------------------------------------------------------- 1 | # Example taken from the paper quoted below. 2 | 3 | # @misc{https://doi.org/10.48550/arxiv.2206.08758, 4 | # doi = {10.48550/ARXIV.2206.08758}, 5 | # url = {https://arxiv.org/abs/2206.08758}, 6 | # author = {Coste-Marquis, Sylvie and Marquis, Pierre}, 7 | # keywords = {Artificial Intelligence (cs.AI), FOS: Computer and information sciences, FOS: Computer and information sciences}, 8 | # title = {Rectifying Mono-Label Boolean Classifiers}, 9 | # publisher = {arXiv}, 10 | # year = {2022}, 11 | # copyright = {Creative Commons Attribution Non Commercial No Derivatives 4.0 International} 12 | # } 13 | 14 | #Let us suppose that the predictor f furnished by the bank labels an instance positive when it corresponds to a customer who 15 | # has high incomes (f1) but has not reimbursed a previous loan (f2), 16 | # or (which looks more risky) a customer who has low incomes (f1) and has some debts (f3). 17 | 18 | from pyxai import Builder, Explainer 19 | 20 | nodeT1_3 = Builder.DecisionNode(3, left=0, right=1) 21 | nodeT1_2 = Builder.DecisionNode(2, left=1, right=0) 22 | nodeT1_1 = Builder.DecisionNode(1, left=nodeT1_2, right=nodeT1_3) 23 | model = Builder.DecisionTree(3, nodeT1_1, force_features_equal_to_binaries=True) 24 | 25 | loan_types = { 26 | "binary": ["f1", "f2", "f3"], 27 | } 28 | 29 | explainer = Explainer.initialize(model, features_type=loan_types) 30 | 31 | print("Original tree:", explainer.get_model().raw_data_for_CPP()) 32 | 33 | #Alice’s expertise can be represented by the formula T = ((x1 ∧ not x3) ⇒ y) ∧ (not x2 ⇒ not y) encoding her two decision rules 34 | explainer.rectify(conditions=(1, -3), label=1) #(x1 ∧ not x3) ⇒ y 35 | explainer.rectify(conditions=(-2, ), label=0) #not x2 ⇒ not y 36 | 37 | rectified_model = explainer.get_model().raw_data_for_CPP() 38 | print("Rectified_model:", rectified_model) 39 | 40 | assert (0, (1, 0, (2, 0, 1))) == rectified_model, "The rectified model is not good." 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /pyxai/examples/DT/builder-rectify2.py: -------------------------------------------------------------------------------- 1 | # Example: 2 | # Instances are described by three attributes: A (numerical), B_1 and B_2 (boolean). 3 | # The value of A gives the annual income of a customer (in k$) 4 | # B_1 indicates whether the customer has no debts, and B_2 indicates that the customer has reimbursed his/her previous loan. 5 | # This is the classifier representing an approximation of the exact function. 6 | # The exact function, which we know, is: the loan is granted if and only if the client's annual income is at least $25k or if the client has no debts and has paid off his previous loan: 7 | # f(x) = 1 <=> (v1 >= 25) and ((v2 == 1) or (v3 == 1)). 8 | # @article{TODO} 9 | # Check V1.0: Ok 10 | 11 | from pyxai import Builder, Explainer 12 | 13 | # Builder part 14 | 15 | node_v3_1 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=1, left=0, right=1) 16 | node_v2_1 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=1, left=0, right=node_v3_1) 17 | 18 | node_v3_2 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=1, left=0, right=1) 19 | node_v2_2 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=1, left=0, right=node_v3_2) 20 | 21 | node_v3_3 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=1, left=0, right=1) 22 | node_v2_3 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=1, left=0, right=node_v3_3) 23 | 24 | node_v1_1 = Builder.DecisionNode(1, operator=Builder.GE, threshold=10, left=node_v2_1, right=node_v2_2) 25 | node_v1_2 = Builder.DecisionNode(1, operator=Builder.GE, threshold=20, left=node_v1_1, right=node_v2_3) 26 | node_v1_3 = Builder.DecisionNode(1, operator=Builder.GE, threshold=30, left=node_v1_2, right=1) 27 | node_v1_4 = Builder.DecisionNode(1, operator=Builder.GE, threshold=40, left=node_v1_3, right=1) 28 | 29 | tree = Builder.DecisionTree(3, node_v1_4) 30 | 31 | print("base:", tree.raw_data_for_CPP()) 32 | loan_types = { 33 | "numerical": ["f1"], 34 | "binary": ["f2", "f3"], 35 | } 36 | 37 | print("bob = (20, 1, 0):") 38 | bob = (20, 1, 0) 39 | explainer = Explainer.initialize(tree, instance=bob, features_type=loan_types) 40 | 41 | print("binary representation: ", explainer.binary_representation) 42 | print("target_prediction:", explainer.target_prediction) 43 | print("to_features:", explainer.to_features(explainer.binary_representation, eliminate_redundant_features=False)) 44 | 45 | minimal = explainer.minimal_sufficient_reason() 46 | 47 | print("Minimal sufficient reason:", minimal) 48 | 49 | explainer.rectify(conditions=minimal, label=1) 50 | print("binary representation: ", explainer.binary_representation) 51 | print("target_prediction:", explainer.target_prediction) 52 | print("to_features:", explainer.to_features(explainer.binary_representation, eliminate_redundant_features=False)) 53 | minimal = explainer.minimal_sufficient_reason() 54 | print("Minimal sufficient reason:", minimal) -------------------------------------------------------------------------------- /pyxai/examples/DT/builder-rectify3.py: -------------------------------------------------------------------------------- 1 | 2 | from pyxai import Builder, Explainer 3 | 4 | # Builder part 5 | 6 | node_L_1 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=1, left=0, right=1) 7 | node_L_2 = Builder.DecisionNode(1, operator=Builder.GT, threshold=20, left=0, right=node_L_1) 8 | 9 | node_R_1 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=1, left=0, right=1) 10 | node_R_2 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=1, left=node_R_1, right=1) 11 | 12 | root = Builder.DecisionNode(1, operator=Builder.GT, threshold=30, left=node_L_2, right=node_R_2) 13 | tree = Builder.DecisionTree(3, root, feature_names=["I", "PP", "R"]) 14 | 15 | print("base:", tree.raw_data_for_CPP()) 16 | loan_types = { 17 | "numerical": ["I"], 18 | "binary": ["PP", "R"], 19 | } 20 | 21 | explainer = Explainer.initialize(tree, instance=(25, 1, 1), features_type=loan_types) 22 | 23 | print("binary representation: ", explainer.binary_representation) 24 | print("target_prediction:", explainer.target_prediction) 25 | print("to_features:", explainer.to_features(explainer.binary_representation, eliminate_redundant_features=False)) 26 | 27 | #For him/her, the following classification rule must be obeyed: 28 | #whenever the annual income of the client is lower than 30, 29 | #the demand should be rejected 30 | rectified_model = explainer.rectify(conditions=(-1, ), label=0) 31 | 32 | assert (0, (1, 0, (4, (3, 0, 1), 1))) == rectified_model.raw_data_for_CPP(), "The rectified model is not good." 33 | -------------------------------------------------------------------------------- /pyxai/examples/DT/simple.py: -------------------------------------------------------------------------------- 1 | # Check V1.0: Ok 2 | from pyxai import Learning, Explainer, Tools 3 | 4 | # Machine learning part 5 | learner = Learning.Scikitlearn(Tools.Options.dataset, learner_type=Learning.CLASSIFICATION) 6 | 7 | model = learner.evaluate(method=Learning.HOLD_OUT, output=Learning.DT) 8 | instance, prediction = learner.get_instances(model, n=1, correct=True) 9 | 10 | # Explanation part 11 | explainer = Explainer.decision_tree(model, instance) 12 | print("instance:", instance) 13 | 14 | if len(explainer.binary_representation) < 15: 15 | print("binary representation: ", explainer.binary_representation) 16 | print("prediction:", prediction) 17 | print("len binary representation", len(explainer.binary_representation)) 18 | 19 | direct_reason = explainer.direct_reason() 20 | print("\nlen direct:", len(direct_reason)) 21 | print("is a reason:", explainer.is_reason(direct_reason)) 22 | 23 | sufficient_reason = explainer.sufficient_reason(n=1) 24 | print("\nlen sufficient reason:", len(sufficient_reason)) 25 | print("to features", explainer.to_features(sufficient_reason)) 26 | print("is sufficient_reason (for max 50 checks): ", explainer.is_sufficient_reason(sufficient_reason, n_samples=50)) 27 | print() 28 | minimal = explainer.minimal_sufficient_reason() 29 | print("\nminimal:", len(minimal)) 30 | print("is sufficient_reason (for max 50 checks): ", explainer.is_sufficient_reason(sufficient_reason, n_samples=50)) 31 | 32 | print("\nnecessary literals: ", explainer.necessary_literals()) 33 | print("\nrelevant literals: ", explainer.relevant_literals()) 34 | 35 | sufficient_reasons_per_attribute = explainer.n_sufficient_reasons_per_attribute() 36 | print("\nsufficient_reasons_per_attribute:", sufficient_reasons_per_attribute) 37 | 38 | constractive_reasons = explainer.contrastive_reason(n=Explainer.ALL) 39 | print("\nnb constractive_reasons:", len(constractive_reasons)) 40 | 41 | all_are_contrastive = True 42 | for contrastive in constractive_reasons: 43 | if not explainer.is_contrastive_reason(contrastive): 44 | print(f"{contrastive} is not a contrastive reason") 45 | all_are_contrastive = False 46 | 47 | if all_are_contrastive: 48 | print("All contrastive are ok") 49 | 50 | explainer.visualisation.gui() 51 | -------------------------------------------------------------------------------- /pyxai/examples/RF/GUI-australian.py: -------------------------------------------------------------------------------- 1 | # Check V1.0: Ok 2 | 3 | from pyxai import Learning, Explainer 4 | 5 | # Machine learning part 6 | learner = Learning.Scikitlearn("examples/datasets_converted/australian_0.csv", learner_type=Learning.CLASSIFICATION) 7 | model = learner.evaluate(method=Learning.HOLD_OUT, output=Learning.RF) 8 | instances = learner.get_instances(model, n=10, seed=11200, correct=True) 9 | 10 | australian_types = { 11 | "numerical": Learning.DEFAULT, 12 | "categorical": {"A4*": (1, 2, 3), 13 | "A5*": tuple(range(1, 15)), 14 | "A6*": (1, 2, 3, 4, 5, 7, 8, 9), 15 | "A12*": tuple(range(1, 4))}, 16 | "binary": ["A1", "A8", "A9", "A11"], 17 | } 18 | 19 | # Explainer part 20 | 21 | explainer = Explainer.initialize(model, features_type=australian_types) 22 | for (instance, prediction) in instances: 23 | explainer.set_instance(instance) 24 | 25 | majoritary_reason = explainer.majoritary_reason(time_limit=10) 26 | print("majoritary_reason", len(majoritary_reason)) 27 | #majoritary_reason = explainer.majoritary_reason(time_limit=50) 28 | #majoritary_reason = explainer.majoritary_reason(time_limit=100) 29 | 30 | 31 | #print("\nlen tree_specific: ", len(majoritary_reason)) 32 | 33 | #print("\ntree_specific without intervales: ", explainer.to_features(majoritary_reason, without_intervals=True)) 34 | #print("\ntree_specific: ", explainer.to_features(majoritary_reason)) 35 | #print("is majoritary:", explainer.is_majoritary_reason(majoritary_reason)) 36 | 37 | explainer.visualisation.gui() -------------------------------------------------------------------------------- /pyxai/examples/RF/GUI-builder-loan.py: -------------------------------------------------------------------------------- 1 | # Check V1.0: Ok 2 | 3 | from pyxai import Builder, Explainer 4 | 5 | node1 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=1, left=0, right=1) 6 | node2 = Builder.DecisionNode(1, operator=Builder.GE, threshold=20, left=0, right=node1) 7 | node3 = Builder.DecisionNode(1, operator=Builder.GE, threshold=30, left=node2, right=1) 8 | 9 | tree1 = Builder.DecisionTree(2, node3) 10 | tree2 = Builder.DecisionTree(2, Builder.LeafNode(1)) 11 | 12 | forest = Builder.RandomForest([tree1, tree2], n_classes=2) 13 | 14 | alice = (18, 0) 15 | explainer = Explainer.initialize(forest, instance=alice) 16 | print("binary representation: ", explainer.binary_representation) 17 | print("binary representation features:", explainer.to_features(explainer.binary_representation, eliminate_redundant_features=False)) 18 | print("target_prediction:", explainer.target_prediction) 19 | 20 | explainer = Explainer.initialize(forest, instance=alice, features_type={"numerical": ["f1"], "binary": ["f2"]}) 21 | 22 | contrastives = explainer.minimal_contrastive_reason(n=Explainer.ALL) 23 | print("contrastives:", contrastives) 24 | print("contrastives (to_features):", explainer.to_features(contrastives[0], contrastive=True)) 25 | print("contrastives (to_features):", explainer.to_features(contrastives[1], contrastive=True)) 26 | 27 | explainer.visualisation.gui() -------------------------------------------------------------------------------- /pyxai/examples/RF/GUI-mnist49.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | import numpy 3 | from pyxai import Learning, Explainer, Tools 4 | 5 | # To use with the minist dataset for example 6 | # (classification between 4 and 9 or between 3 and 8) 7 | # available here: 8 | # http://www.cril.univ-artois.fr/expekctation/datasets/mnist38.csv 9 | # http://www.cril.univ-artois.fr/expekctation/datasets/mnist49.csv 10 | # Check V1.0: Ok 11 | 12 | # the location of the dataset 13 | dataset = "../datasets_ijcai/mnist49.csv" 14 | 15 | # Machine learning part 16 | learner = Learning.Scikitlearn(dataset, learner_type=Learning.CLASSIFICATION) 17 | model = learner.evaluate(method=Learning.HOLD_OUT, output=Learning.RF) 18 | instances = learner.get_instances(model, n=10, correct=True) 19 | 20 | # Explanation part 21 | explainer = Explainer.initialize(model) 22 | 23 | for (instance, prediction) in instances: 24 | explainer.set_instance(instance) 25 | direct = explainer.direct_reason() 26 | print("len direct:", len(direct)) 27 | 28 | majoritary_reason = explainer.majoritary_reason(n=1) 29 | print("len majoritary_reason:", len(majoritary_reason)) 30 | #assert explainer.is_reason(majoritary_reason), "This is not a reason reason !" 31 | 32 | # minimal_majoritary = explainer 33 | minimal_reason = explainer.minimal_majoritary_reason(time_limit=60) 34 | #assert explainer.is_reason(minimal_reason), "This is not a reason" 35 | if explainer.elapsed_time == Explainer.TIMEOUT: print("This is an approximation") 36 | print("len minimal majoritary reason:", len(minimal_reason)) 37 | 38 | 39 | # Visualization part 40 | def get_pixel_value(instance, x, y, shape): 41 | index = x * shape[0] + y 42 | return instance[index] 43 | 44 | def instance_index_to_pixel_position(i, shape): 45 | return i // shape[0], i % shape[0] 46 | 47 | explainer.visualisation.gui(image={"shape": (28,28), 48 | "dtype": numpy.uint8, 49 | "get_pixel_value": get_pixel_value, 50 | "instance_index_to_pixel_position": instance_index_to_pixel_position}) 51 | 52 | -------------------------------------------------------------------------------- /pyxai/examples/RF/builder-anchored2.py: -------------------------------------------------------------------------------- 1 | 2 | from pyxai import Builder, Explainer 3 | 4 | node_t1_v1_1 = Builder.DecisionNode(1, operator=Builder.GE, threshold=10, left=0, right=0) 5 | node_t1_v1_2 = Builder.DecisionNode(1, operator=Builder.GE, threshold=20, left=node_t1_v1_1, right=0) 6 | node_t1_v1_3 = Builder.DecisionNode(1, operator=Builder.GE, threshold=30, left=node_t1_v1_2, right=1) 7 | node_t1_v1_4 = Builder.DecisionNode(1, operator=Builder.GE, threshold=40, left=node_t1_v1_3, right=1) 8 | tree_1 = Builder.DecisionTree(3, node_t1_v1_4) 9 | 10 | node_t2_v3_1 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=1, left=0, right=1) 11 | node_t2_v3_2 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=1, left=0, right=node_t2_v3_1) 12 | #node_t2_v3_3 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=3, left=node_t2_v3_2, right=1) 13 | #node_t2_v2 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=1, left=0, right=node_t2_v3_3) 14 | 15 | tree_2 = Builder.DecisionTree(3, node_t2_v3_2) 16 | 17 | tree_3 = Builder.DecisionTree(3, Builder.LeafNode(1)) 18 | 19 | forest = Builder.RandomForest([tree_1, tree_2, tree_3], n_classes=2) 20 | 21 | explainer = Explainer.initialize(forest) 22 | 23 | # check part 24 | for a in [0, 1]: 25 | for b in [0, 1]: 26 | for c in [0, 1]: 27 | 28 | instance = (a, b, c) 29 | #if instance == (0,1,1): 30 | explainer.set_instance(instance=instance) 31 | print("-------------------------") 32 | print("intance:", instance) 33 | print("binary representation: ", explainer.binary_representation) 34 | print("target_prediction:", explainer.target_prediction) 35 | print("to_features:", explainer.to_features(explainer.binary_representation, eliminate_redundant_features=False)) 36 | if explainer.target_prediction == 0: 37 | reference_instances = {0:[instance], 1:[[0 if l==1 else 1 for l in instance]]} 38 | else: 39 | reference_instances = {0:[[0 if l==1 else 1 for l in instance]], 1:[instance]} 40 | print("reference_instances:", reference_instances) 41 | 42 | n_anchors = 1 43 | go_next = True 44 | while(go_next is True): 45 | anchored_reason = explainer.anchored_reason(n_anchors=n_anchors, reference_instances=reference_instances, check=True) 46 | print(str(n_anchors)+"-anchored_reason:"+str(anchored_reason)) 47 | if anchored_reason is not None: 48 | go_next = True 49 | n_anchors += 1 50 | else: 51 | break -------------------------------------------------------------------------------- /pyxai/examples/RF/builder-categorical-paper.py: -------------------------------------------------------------------------------- 1 | # Example: 2 | # Instances are described by three attributes: A (numerical), B_1 and B_2 (boolean). 3 | # The value of A gives the annual income of a customer (in k$) 4 | # B_1 indicates whether the customer has no debts, and B_2 indicates that the customer has reimbursed his/her previous loan. 5 | # This is the classifier representing an approximation of the exact function. 6 | # The exact function, which we know, is: the loan is granted if and only if the client's annual income is at least $25k or if the client has no debts and has paid off his previous loan: f(x) = 1 <=> (v1 >= 25) and ((v2 == 1) or (v3 == 1)). 7 | # @article{TODO} 8 | # Check V1.0: Ok 9 | from pyxai import Builder, Explainer, Learning 10 | 11 | # Builder part 12 | 13 | node_t1_v1_1 = Builder.DecisionNode(1, operator=Builder.GE, threshold=10, left=0, right=0) 14 | node_t1_v1_2 = Builder.DecisionNode(1, operator=Builder.GE, threshold=20, left=node_t1_v1_1, right=0) 15 | node_t1_v1_3 = Builder.DecisionNode(1, operator=Builder.GE, threshold=30, left=node_t1_v1_2, right=1) 16 | node_t1_v1_4 = Builder.DecisionNode(1, operator=Builder.GE, threshold=40, left=node_t1_v1_3, right=1) 17 | tree_1 = Builder.DecisionTree(3, node_t1_v1_4) 18 | 19 | node_t2_v3_1 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=1, left=0, right=1) 20 | node_t2_v3_2 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=1, left=0, right=node_t2_v3_1) 21 | #node_t2_v3_3 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=3, left=node_t2_v3_2, right=1) 22 | #node_t2_v2 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=1, left=0, right=node_t2_v3_3) 23 | 24 | tree_2 = Builder.DecisionTree(3, node_t2_v3_2) 25 | 26 | tree_3 = Builder.DecisionTree(3, Builder.LeafNode(1)) 27 | 28 | forest = Builder.RandomForest([tree_1, tree_2, tree_3], n_classes=2) 29 | #forest.add_numerical_feature(1) 30 | #forest.add_categorical_feature(3) 31 | 32 | 33 | print("numerical_features:", forest.numerical_features) 34 | 35 | print("bob = (20, 1, 0):") 36 | bob = (20, 1, 0) 37 | explainer = Explainer.initialize(forest, instance=bob, features_type={"numerical":["f1"], "binary":Learning.DEFAULT}) 38 | 39 | print("binary representation: ", explainer.binary_representation) 40 | print("target_prediction:", explainer.target_prediction) 41 | print("to_features:", explainer.to_features(explainer.binary_representation, eliminate_redundant_features=False)) 42 | 43 | minimals = explainer.minimal_sufficient_reason() 44 | print("Minimal sufficient reasons:", minimals) 45 | print("to_features:", explainer.to_features(minimals, eliminate_redundant_features=True)) 46 | 47 | print() 48 | print("charles = (5, 0, 0):") 49 | charles = (5, 0, 0) 50 | explainer.set_instance(charles) 51 | 52 | print("binary representation: ", explainer.binary_representation) 53 | print("target_prediction:", explainer.target_prediction) 54 | print("to_features:", explainer.to_features(explainer.binary_representation, eliminate_redundant_features=False)) 55 | 56 | contrastive = explainer.minimal_contrastive_reason(n=1) 57 | print("contrastive:", contrastive) 58 | 59 | print("contrastive (to_features):", explainer.to_features(contrastive, contrastive=True)) 60 | print("is contrastive:", explainer.is_contrastive_reason(contrastive)) 61 | 62 | explainer.visualisation.gui() -------------------------------------------------------------------------------- /pyxai/examples/RF/builder-categorical.py: -------------------------------------------------------------------------------- 1 | # Example: 2 | # Instances are described by three attributes: A (numerical), B_1 and B_2 (boolean). 3 | # The value of A gives the annual income of a customer (in k$) 4 | # B_1 indicates whether the customer has no debts, and B_2 indicates that the customer has reimbursed his/her previous loan. 5 | # This is the classifier representing an approximation of the exact function. 6 | # The exact function, which we know, is: the loan is granted if and only if the client's annual income is at least $25k or if the client has no debts and has paid off his previous loan: f(x) = 1 <=> (v1 >= 25) and ((v2 == 1) or (v3 == 1)). 7 | # @article{TODO} 8 | # Check V1.0: Ok but => Only one-hot encoded categorical features are take into account. 9 | from pyxai import Builder, Learning, Explainer 10 | 11 | # Builder part 12 | 13 | node_t1_v1_1 = Builder.DecisionNode(1, operator=Builder.GE, threshold=10, left=0, right=0) 14 | node_t1_v1_2 = Builder.DecisionNode(1, operator=Builder.GE, threshold=20, left=node_t1_v1_1, right=0) 15 | node_t1_v1_3 = Builder.DecisionNode(1, operator=Builder.GE, threshold=30, left=node_t1_v1_2, right=1) 16 | node_t1_v1_4 = Builder.DecisionNode(1, operator=Builder.GE, threshold=40, left=node_t1_v1_3, right=1) 17 | tree_1 = Builder.DecisionTree(3, node_t1_v1_4) 18 | 19 | node_t2_v3_1 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=1, left=0, right=1) 20 | node_t2_v3_2 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=2, left=node_t2_v3_1, right=1) 21 | node_t2_v3_3 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=3, left=node_t2_v3_2, right=1) 22 | 23 | node_t2_v2 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=1, left=0, right=node_t2_v3_3) 24 | 25 | tree_2 = Builder.DecisionTree(3, node_t2_v2) 26 | 27 | tree_3 = Builder.DecisionTree(3, Builder.LeafNode(1)) 28 | 29 | forest = Builder.RandomForest([tree_1, tree_2, tree_3], n_classes=2) 30 | 31 | print("bob = (20, 1, 0):") 32 | bob = (20, 1, 0) 33 | explainer = Explainer.initialize(forest, instance=bob, features_type={"numerical":["f1"], "categorical": {"f3":['1','2','3']}, "binary":Learning.DEFAULT}) 34 | 35 | print("binary representation: ", explainer.binary_representation) 36 | print("target_prediction:", explainer.target_prediction) 37 | print("to_features:", explainer.to_features(explainer.binary_representation, eliminate_redundant_features=False)) 38 | 39 | minimals = explainer.minimal_sufficient_reason() 40 | print("Minimal sufficient reasons:", minimals) 41 | print("to_features:", explainer.to_features(minimals)) 42 | 43 | print() 44 | print("charles = (30, 0, 2):") 45 | charles = (30, 0, 2) 46 | explainer.set_instance(charles) 47 | 48 | print("binary representation: ", explainer.binary_representation) 49 | print("target_prediction:", explainer.target_prediction) 50 | print("to_features:", explainer.to_features(explainer.binary_representation, eliminate_redundant_features=False)) 51 | 52 | contrastives = explainer.minimal_contrastive_reason(n=Explainer.ALL) 53 | print("contrastives:", contrastives) 54 | 55 | print("contrastives (to_features):", explainer.to_features(contrastives[0], contrastive=True)) 56 | 57 | -------------------------------------------------------------------------------- /pyxai/examples/RF/builder-loan-simple.py: -------------------------------------------------------------------------------- 1 | 2 | # Check V1.0: Ok 3 | 4 | from pyxai import Builder, Explainer 5 | 6 | node1 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=1, left=0, right=1) 7 | node2 = Builder.DecisionNode(1, operator=Builder.GE, threshold=20, left=0, right=node1) 8 | node3 = Builder.DecisionNode(1, operator=Builder.GE, threshold=30, left=node2, right=1) 9 | 10 | tree1 = Builder.DecisionTree(2, node3) 11 | tree2 = Builder.DecisionTree(2, Builder.LeafNode(1)) 12 | 13 | forest = Builder.RandomForest([tree1, tree2], n_classes=2) 14 | 15 | alice = (18, 0) 16 | explainer = Explainer.initialize(forest, instance=alice) 17 | print("binary representation: ", explainer.binary_representation) 18 | print("binary representation features:", explainer.to_features(explainer.binary_representation, eliminate_redundant_features=False)) 19 | print("target_prediction:", explainer.target_prediction) 20 | 21 | explainer = Explainer.initialize(forest, instance=alice, features_type={"numerical": ["f1"], "binary": ["f2"]}) 22 | 23 | contrastives = explainer.minimal_contrastive_reason(n=Explainer.ALL) 24 | print("contrastives:", contrastives) 25 | print("contrastives (to_features):", explainer.to_features(contrastives[0], contrastive=True)) 26 | print("contrastives (to_features):", explainer.to_features(contrastives[1], contrastive=True)) 27 | 28 | -------------------------------------------------------------------------------- /pyxai/examples/RF/builder-loan.py: -------------------------------------------------------------------------------- 1 | # Example: 2 | # Instances are described by three attributes: A (numerical), B_1 and B_2 (boolean). 3 | # The value of A gives the annual income of a customer (in k$) 4 | # B_1 indicates whether the customer has no debts, and B_2 indicates that the customer has reimbursed his/her previous loan. 5 | # This is the classifier representing an approximation of the exact function. 6 | # The exact function, which we know, is: the loan is granted if and only if the client's annual income is at least $25k 7 | # or if the client has no debts and has paid off his previous loan: f(x) = 1 <=> (v1 >= 25) and ((v2 == 1) or (v3 == 1)). 8 | # @article{TODO} 9 | # Check V1.0: Ok 10 | 11 | from pyxai import Builder, Explainer 12 | 13 | # Builder part 14 | 15 | node_t1_v1_1 = Builder.DecisionNode(1, operator=Builder.GE, threshold=10, left=0, right=0) 16 | node_t1_v1_2 = Builder.DecisionNode(1, operator=Builder.GE, threshold=20, left=node_t1_v1_1, right=0) 17 | node_t1_v1_3 = Builder.DecisionNode(1, operator=Builder.GE, threshold=30, left=node_t1_v1_2, right=1) 18 | node_t1_v1_4 = Builder.DecisionNode(1, operator=Builder.GE, threshold=40, left=node_t1_v1_3, right=1) 19 | tree_1 = Builder.DecisionTree(3, node_t1_v1_4) 20 | 21 | node_t2_v3 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=1, left=0, right=1) 22 | node_t2_v2 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=1, left=0, right=node_t2_v3) 23 | tree_2 = Builder.DecisionTree(3, node_t2_v2) 24 | 25 | tree_3 = Builder.DecisionTree(3, Builder.LeafNode(1)) 26 | 27 | forest = Builder.RandomForest([tree_1, tree_2, tree_3], n_classes=2) 28 | 29 | bob = (20, 1, 0) 30 | explainer = Explainer.initialize(forest, instance=bob, features_type={"numerical": ["f1"], "binary": ["f2", "f3"]}) 31 | 32 | print("binary representation: ", explainer.binary_representation) 33 | print("target_prediction:", explainer.target_prediction) 34 | print("to_features:", explainer.to_features(explainer.binary_representation, eliminate_redundant_features=False)) 35 | 36 | minimals = explainer.minimal_sufficient_reason() 37 | print("Minimal sufficient reasons:", minimals) 38 | print("to_features:", explainer.to_features(minimals, eliminate_redundant_features=False)) 39 | 40 | print() 41 | print("charles = (5, 0, 0):") 42 | charles = (5, 0, 0) 43 | explainer.set_instance(charles) 44 | 45 | print("binary representation: ", explainer.binary_representation) 46 | print("target_prediction:", explainer.target_prediction) 47 | print("to_features:", explainer.to_features(explainer.binary_representation, eliminate_redundant_features=False)) 48 | 49 | contrastives = explainer.minimal_contrastive_reason(n=Explainer.ALL) 50 | print("contrastives:", contrastives) 51 | 52 | print("contrastives (to_features):", explainer.to_features(contrastives[0], contrastive=True)) 53 | 54 | -------------------------------------------------------------------------------- /pyxai/examples/RF/builder-multiclasses.py: -------------------------------------------------------------------------------- 1 | # Check V1.0: Ok but minimal_majoritary_reason() return () problem 2 | 3 | from pyxai import Builder, Explainer 4 | 5 | node_1_1 = Builder.DecisionNode(4, operator=Builder.EQ, threshold=1, left=3, right=2) 6 | node_1_2 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=1, left=1, right=node_1_1) 7 | node_1_3 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=1, left=1, right=node_1_2) 8 | node_1_4 = Builder.DecisionNode(1, operator=Builder.EQ, threshold=1, left=3, right=node_1_3) 9 | tree_1 = Builder.DecisionTree(6, node_1_4) 10 | 11 | node_2_1 = Builder.DecisionNode(2, operator=Builder.EQ, threshold=1, left=1, right=2) 12 | node_2_2 = Builder.DecisionNode(4, operator=Builder.EQ, threshold=1, left=3, right=node_2_1) 13 | node_2_3 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=1, left=node_2_2, right=3) 14 | tree_2 = Builder.DecisionTree(6, node_2_3) 15 | 16 | node_3_1 = Builder.DecisionNode(1, operator=Builder.EQ, threshold=1, left=3, right=1) 17 | node_3_2 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=1, left=2, right=4) 18 | node_3_3 = Builder.DecisionNode(5, operator=Builder.EQ, threshold=1, left=2, right=node_3_2) 19 | node_3_4 = Builder.DecisionNode(4, operator=Builder.EQ, threshold=1, left=node_3_1, right=node_3_3) 20 | tree_3 = Builder.DecisionTree(6, node_3_4) 21 | 22 | node_4_1 = Builder.DecisionNode(6, operator=Builder.EQ, threshold=1, left=4, right=3) 23 | tree_4 = Builder.DecisionTree(6, node_4_1) 24 | 25 | node_5_1 = Builder.DecisionNode(3, operator=Builder.EQ, threshold=1, left=2, right=4) 26 | node_5_2 = Builder.DecisionNode(4, operator=Builder.EQ, threshold=1, left=3, right=node_5_1) 27 | node_5_3 = Builder.DecisionNode(1, operator=Builder.EQ, threshold=1, left=1, right=node_5_2) 28 | tree_5 = Builder.DecisionTree(6, node_5_3) 29 | 30 | forest = Builder.RandomForest([tree_1, tree_2, tree_3, tree_4, tree_5], n_classes=5) 31 | 32 | instance = [0, 0, 1, 1, 0, 0] 33 | instance = [0, 0, 0, 0, 0, 0] 34 | instance = [1, 0, 0, 0, 1, 1] 35 | explainer = Explainer.initialize(forest, instance=instance) 36 | print("instance", instance) 37 | print("binary", explainer.binary_representation) 38 | print("to f", explainer.to_features(explainer.binary_representation, eliminate_redundant_features=False)) 39 | print("prediction: ", explainer.target_prediction) 40 | 41 | print("direct reason: ", explainer.direct_reason()) 42 | print("majoritary reason: ", explainer.majoritary_reason(seed=6, n_iterations=1)) 43 | minimal_majoritary_reason = explainer.minimal_majoritary_reason() 44 | print("minimal majoritary reason: ", minimal_majoritary_reason) 45 | print("is majoritary", explainer.is_majoritary_reason(minimal_majoritary_reason)) 46 | print("sufficient reason: ", explainer.sufficient_reason()) 47 | 48 | instance = [0, 0, 0, 1, 1, 1] # the same number of votes :( 49 | explainer.set_instance(instance) 50 | print("instance", instance) 51 | print("prediction: ", explainer.target_prediction) 52 | 53 | print("direct_reason: ", explainer.direct_reason()) 54 | print("majoritary reason: ", explainer.majoritary_reason(seed=6, n_iterations=1)) 55 | minimal_majoritary_reason = explainer.minimal_majoritary_reason(n=1) 56 | print("minimal majoritary reason: ", minimal_majoritary_reason) 57 | 58 | if minimal_majoritary_reason is not None: 59 | print("is majoritary", explainer.is_majoritary_reason(minimal_majoritary_reason)) 60 | print("majoritary reason: ", explainer.majoritary_reason(seed=6, n_iterations=1)) 61 | 62 | sufficient_reason = explainer.sufficient_reason() 63 | print("sufficient reason: ", sufficient_reason) 64 | -------------------------------------------------------------------------------- /pyxai/examples/RF/simple.py: -------------------------------------------------------------------------------- 1 | from pyxai import Learning, Explainer, Tools 2 | 3 | # usage 4 | # python3 pyxai/examples/RF/Simple.py -dataset=path/to/dataset.csv 5 | # Check V1.0: Ok 6 | 7 | # Machine learning part 8 | learner = Learning.Scikitlearn(Tools.Options.dataset, learner_type=Learning.CLASSIFICATION) 9 | model = learner.evaluate(method=Learning.HOLD_OUT, output=Learning.RF) 10 | instance, prediction = learner.get_instances(n=1) 11 | 12 | print("instance:", instance) 13 | 14 | # Explainer part 15 | explainer = Explainer.initialize(model, instance=instance, features_type=Tools.Options.types) 16 | 17 | direct_reason = explainer.direct_reason() 18 | print("len direct:", len(direct_reason)) 19 | print("is a reason (for 50 checks):", explainer.is_reason(direct_reason, n_samples=50)) 20 | 21 | majoritary = explainer.majoritary_reason(n=1, n_iterations=1) 22 | print("\nmajoritary: ", explainer.to_features(majoritary)) 23 | print("\nlen majoritary: ", len(majoritary)) 24 | print("is_majoritary_reason (for 50 checks):", explainer.is_majoritary_reason(majoritary)) 25 | 26 | minimal_majoritary = explainer.minimal_majoritary_reason(time_limit=10) 27 | 28 | print("\nminimal majoritary: ", explainer.to_features(minimal_majoritary)) 29 | print("len majoritary: ", len(minimal_majoritary)) 30 | print("is_majoritary_reason (for 50 checks):", explainer.is_majoritary_reason(minimal_majoritary)) 31 | 32 | 33 | # can be costly 34 | sufficient_reason = explainer.sufficient_reason(time_limit=5) 35 | print("\nlen sufficient reason:", len(sufficient_reason)) 36 | if explainer.elapsed_time == Explainer.TIMEOUT: print("Time out, this is an approximation") 37 | print("sufficient: ", explainer.to_features(sufficient_reason)) 38 | print("is reason (for 50 checks)", explainer.is_reason(sufficient_reason, n_samples=50)) 39 | 40 | minimal_constrative_reason = explainer.minimal_contrastive_reason(time_limit=5) 41 | if len(minimal_constrative_reason) == 0: 42 | print("\nminimal contrastive not found") 43 | else: 44 | print("\nminimal contrastive: ", len(minimal_constrative_reason)) 45 | if explainer.elapsed_time == Explainer.TIMEOUT: print("Time out, this is an approximation") 46 | print("is contrastive: ", explainer.is_contrastive_reason(minimal_constrative_reason)) 47 | 48 | explainer.visualisation.gui() -------------------------------------------------------------------------------- /pyxai/examples/RF/theories-majoritary.py: -------------------------------------------------------------------------------- 1 | 2 | #python3 examples/RF/theories-majoritary.py -dataset=../../data/dataMLothers/iris.csv 3 | ## Check V1.0: Ok but bug in explainer.majoritary_reason(n_iterations=10): free(): invalid pointer Abandon (core dumped) 4 | 5 | from pyxai import Learning, Explainer, Tools 6 | 7 | # Machine learning part 8 | learner = Learning.Scikitlearn(Tools.Options.dataset, learner_type=Learning.CLASSIFICATION) 9 | model = learner.evaluate(method=Learning.HOLD_OUT, output=Learning.RF) 10 | instance, prediction = learner.get_instances(n=1) 11 | 12 | print("instance:", instance) 13 | 14 | # Explainer part 15 | print("No theory") 16 | explainer = Explainer.initialize(model, instance=instance) 17 | majoritary_reason = explainer.majoritary_reason(n_iterations=10) 18 | print("majoritary:", majoritary_reason) 19 | 20 | print("\nlen majoritary: ", len(majoritary_reason)) 21 | print("\nmajoritary: ", explainer.to_features(majoritary_reason, eliminate_redundant_features=False)) 22 | print("is a majoritary", explainer.is_majoritary_reason(majoritary_reason)) 23 | 24 | sufficient_reason = explainer.sufficient_reason(time_limit=5) 25 | print("sufficient_reason:", sufficient_reason) 26 | print("\nlen sufficient reason:", len(sufficient_reason)) 27 | if explainer.elapsed_time == Explainer.TIMEOUT: print("Time out, this is an approximation") 28 | print("is reason (for 50 checks)", explainer.is_sufficient_reason(sufficient_reason, n_samples=1050)) 29 | 30 | 31 | print("instance: ", instance) 32 | print("\n\n---------------------------------------------\nTheory") 33 | 34 | 35 | explainer = Explainer.initialize(model, instance=instance, features_type={"numerical": Learning.DEFAULT}) 36 | 37 | direct_reason = explainer.direct_reason() 38 | print("len direct:", len(direct_reason)) 39 | #print("is a reason (for 50 checks):", explainer.is_reason(direct_reason, n_samples=50)) 40 | 41 | majoritary = explainer.majoritary_reason(n=1, n_iterations=1) 42 | print("majoritary:", majoritary) 43 | 44 | print("\nmajoritary: ", explainer.to_features(majoritary, eliminate_redundant_features=False)) 45 | print("\nmajoritary: ", explainer.to_features(majoritary)) 46 | 47 | print("\nlen majoritary: ", len(majoritary)) 48 | 49 | print("is_majoritary_reason (for 50 checks):", explainer.is_majoritary_reason(majoritary)) 50 | 51 | # can be costly 52 | sufficient_reason = explainer.sufficient_reason(time_limit=5) 53 | print("sufficient_reason=", explainer.to_features(sufficient_reason)) 54 | print("\nlen sufficient reason:", len(sufficient_reason)) 55 | if explainer.elapsed_time == Explainer.TIMEOUT: print("Time out, this is an approximation") 56 | print("is reason (for 50 checks)", explainer.is_reason(sufficient_reason, n_samples=50)) 57 | 58 | minimal_constrative_reason = explainer.minimal_contrastive_reason(time_limit=5) 59 | if len(minimal_constrative_reason) == 0: 60 | print("\nminimal contrastive not found") 61 | else: 62 | print("\nminimal contrastive: ", len(minimal_constrative_reason)) 63 | if explainer.elapsed_time == Explainer.TIMEOUT: print("Time out, this is an approximation") 64 | print("is contrastive: ", explainer.is_contrastive_reason(minimal_constrative_reason)) 65 | 66 | explainer.visualisation.gui() -------------------------------------------------------------------------------- /pyxai/examples/RF/theories-types-dict.py: -------------------------------------------------------------------------------- 1 | from pyxai import Learning, Explainer, Tools 2 | 3 | # usage 4 | # python3 examples/RF/theories-types-file.py -dataset=examples/datasets_converted/australian_0.csv -types=examples/datasets_converted/australian_0.types 5 | # Check V1.0: Ok 6 | 7 | # Machine learning part 8 | learner = Learning.Scikitlearn(Tools.Options.dataset, learner_type=Learning.CLASSIFICATION) 9 | model = learner.evaluate(method=Learning.HOLD_OUT, output=Learning.RF) 10 | instances = learner.get_instances(model, n=2) 11 | 12 | #print("instance:", instance) 13 | 14 | 15 | # Explainer part 16 | australian_types = { 17 | "numerical": Learning.DEFAULT, 18 | "categorical": {"A4*": (1, 2, 3), 19 | "A5*": tuple(range(1, 15)), 20 | "A6*": (1, 2, 3, 4, 5, 7, 8, 9), 21 | "A12*": tuple(range(1, 4))}, 22 | "binary": ["A1", "A8", "A9", "A11"], 23 | } 24 | 25 | explainer = Explainer.initialize(model, features_type=australian_types) 26 | for (instance, prediction) in instances: 27 | explainer.set_instance(instance) 28 | 29 | majoritary_reason = explainer.majoritary_reason(n_iterations=10) 30 | print("\nlen tree_specific: ", len(majoritary_reason)) 31 | print("\ntree_specific: ", explainer.to_features(majoritary_reason, eliminate_redundant_features=True)) 32 | print("is a tree specific", explainer.is_majoritary_reason(majoritary_reason)) 33 | 34 | contrastive = explainer.minimal_contrastive_reason(time_limit=100) 35 | features = explainer.to_features(contrastive, contrastive=True) 36 | 37 | print("contrastive:", contrastive) 38 | print("features contrastive:", features) 39 | 40 | -------------------------------------------------------------------------------- /pyxai/examples/RF/theories-types-file.py: -------------------------------------------------------------------------------- 1 | from pyxai import Learning, Explainer, Tools 2 | 3 | # usage 4 | # python3 examples/RF/theories-types-file.py -dataset=examples/datasets_converted/australian_0.csv -types=examples/datasets_converted/australian_0.types 5 | # Check V1.0: Ok 6 | 7 | # Machine learning part 8 | learner = Learning.Scikitlearn(Tools.Options.dataset, learner_type=Learning.CLASSIFICATION) 9 | model = learner.evaluate(method=Learning.HOLD_OUT, output=Learning.RF) 10 | instances = learner.get_instances(n=10) 11 | 12 | 13 | 14 | # Explainer part 15 | #explainer = Explainer.initialize(model, instance=instance, features_type=) 16 | 17 | explainer = Explainer.initialize(model, features_type=Tools.Options.types) 18 | for (instance, prediction) in instances: 19 | explainer.set_instance(instance) 20 | 21 | 22 | #contrastive = explainer.minimal_contrastive_reason(time_limit=100) 23 | #features = explainer.to_features(contrastive, contrastive=True) 24 | 25 | #print("contrastive:", contrastive) 26 | #print("features contrastive:", features) 27 | 28 | majoritary_reason = explainer.majoritary_reason(n_iterations=10) 29 | print("10") 30 | majoritary_reason = explainer.majoritary_reason(n_iterations=100) 31 | print("100") 32 | majoritary_reason = explainer.majoritary_reason(n_iterations=500) 33 | print("500") 34 | features = explainer.to_features(majoritary_reason) 35 | #print("features majoritary:", features) 36 | 37 | explainer.visualisation.gui() 38 | 39 | -------------------------------------------------------------------------------- /pyxai/examples/xai24/constants.py: -------------------------------------------------------------------------------- 1 | from pyxai import Learning 2 | training_size = 0.3 3 | 4 | user_size = 0.25 # ratio with remaining instances 5 | interaction_size = 0.25 6 | test_size = 0.5 7 | 8 | delta = 0.7 #ratio to do a rule according to the number of votes of the forest 9 | theta = 0.5 10 | N = 1000000 11 | debug = True 12 | trace = True 13 | 14 | USER_BT = 0 15 | USER_LAMBDA = 1 16 | user = USER_LAMBDA 17 | 18 | model = Learning.RF 19 | n_iterations = 50 20 | max_time = 3600*3 # 4 hours 21 | 22 | statistics = {"n_initial_positives": 0, "n_initial_negatives": 0, "rectifications": 0, "generalisations": 0, "cases_1": 0, "cases_2": 0, "cases_3": 0, "cases_4": 0, 23 | "cases_5": 0, "n_positives": 0, "n_negatives": 0, "rectifications_times": [], "rectifications_cases":[]} 24 | -------------------------------------------------------------------------------- /pyxai/examples/xai24/coverage.py: -------------------------------------------------------------------------------- 1 | from pyxai.sources.solvers.COMPILER.D4Solver import D4Solver 2 | import random 3 | 4 | # ------------------------------------------------------------------------------------- 5 | # Compute coverage 6 | 7 | 8 | class Coverage: 9 | 10 | def __init__(self, sigma, nb_v, time_limit, user): 11 | self.first_call = True 12 | self.nb_models_in_theory = None 13 | self.nb_variables = nb_v 14 | self.time_limit = time_limit 15 | self.sigma = sigma 16 | self.user = user 17 | 18 | def number_of_models_for_rules(self, rules): 19 | # Special cases 20 | if len(rules) == 0: 21 | return 0 22 | if len(rules) == 1 and len(rules[0]) == 0: 23 | return self.nb_models_in_theory 24 | 25 | 26 | compiler = D4Solver(filenames="/tmp/rules"+str(random.randint(1,100000))) 27 | cnf = self.sigma.copy() 28 | aux = self.nb_variables 29 | new_clause = [] 30 | for rule in rules: 31 | if len(rule) == 1: 32 | new_clause.append(rule[0]) 33 | continue 34 | aux += 1 35 | for lit in rule: 36 | cnf.append([-aux, lit]) 37 | cnf.append([aux] + [-lit for lit in rule]) 38 | new_clause.append(aux) 39 | 40 | cnf.append(new_clause) 41 | compiler.add_cnf(cnf, aux) 42 | 43 | return compiler.count(time_limit=self.time_limit) 44 | 45 | def coverage(self): 46 | if self.first_call: # Compute once the number of model of the theory 47 | first_call = False 48 | models_in_sigma = D4Solver(filenames="/tmp/sigma-") 49 | models_in_sigma.add_cnf(self.sigma, self.nb_variables) 50 | self.nb_models_in_theory = models_in_sigma.count(time_limit=self.time_limit) 51 | if self.nb_models_in_theory is None: 52 | return None 53 | 54 | nb_pos = self.number_of_models_for_rules(self.user.positive_rules) 55 | if nb_pos is None: 56 | return None 57 | nb_neg = self.number_of_models_for_rules(self.user.negative_rules) 58 | if nb_neg is None: 59 | return None 60 | tmp = (nb_pos + nb_neg) / self.nb_models_in_theory 61 | if tmp > 1.0: 62 | print(nb_pos, nb_neg, self.nb_models_in_theory) 63 | sys.exit(1) 64 | assert(tmp <= 1.0) 65 | return tmp 66 | 67 | 68 | def test(self, sigma, positives, negatives, nb_variables): 69 | models_in_sigma = D4Solver(filenames="/tmp/sigma-") 70 | models_in_sigma.add_cnf(self.sigma, self.nb_variables) 71 | nb_sigma = models_in_sigma.count() 72 | nb_pos = self.number_of_models_for_rules(positives) 73 | nb_neg = self.number_of_models_for_rules(negatives) 74 | print(nb_sigma, nb_pos, nb_neg) 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /pyxai/examples/xai24/model.py: -------------------------------------------------------------------------------- 1 | import constants 2 | from pyxai import Learning, Explainer 3 | 4 | class Model: 5 | def __init__(self, learner_AI): 6 | self.learner = learner_AI 7 | self.model = learner_AI.evaluate(method=Learning.HOLD_OUT, output=constants.model, test_size=1 - constants.training_size, seed=123, min_samples_split=0.5) 8 | self.explainer = None 9 | def set_instance(self, instance): 10 | self.explainer.set_instance(instance) 11 | 12 | 13 | def reason(self, *, n_iterations=None): 14 | if constants.model == Learning.RF: 15 | if n_iterations is None: 16 | n_iterations = constants.n_iterations 17 | return self.explainer.majoritary_reason(n_iterations=n_iterations, seed=123) 18 | if constants.model == Learning.DT: 19 | reason = self.explainer.sufficient_reason() 20 | return reason 21 | return None 22 | 23 | def predict_instance(self, instance): 24 | return self.model.predict_instance(instance) 25 | 26 | -------------------------------------------------------------------------------- /pyxai/examples/xai24/tests.py: -------------------------------------------------------------------------------- 1 | import user as u 2 | import coverage 3 | # Generalize 4 | print("check generalize") 5 | assert(u.generalize([1], [2, 1])) 6 | assert(u.generalize([1], [1, -2])) 7 | assert(u.generalize([1, -2], [1, -2])) 8 | assert(u.generalize([1, -2], [2, 4, 5]) is False) 9 | assert(u.generalize([1], [2, 4, 5]) is False) 10 | 11 | 12 | # Check conflict 13 | print("check conflict") 14 | # Conflict 15 | rule1 = [2, 3] 16 | rule2 = [4, 2] 17 | assert(u.conflict(rule1, rule2)) 18 | assert(u.conflict(rule2, rule1)) 19 | assert(u.conflict([-2], [3])) 20 | assert(u.conflict([2], [-2]) is False) 21 | assert(u.conflict([-2], [2]) is False) 22 | 23 | 24 | 25 | 26 | rule = (-12, 16, -22, 30, 41, -99, -127, 199, 218, 226, -273, -296, -345) 27 | ia = (2, 16, 21, 31, -37, 112, 406) 28 | print("ici ", u.conflict(rule, ia)) 29 | 30 | print("coverage") 31 | sigma = [[1, -2], [-3, -4]] 32 | nb_v = 5 33 | positives = [[2, 4], [1, 5]] 34 | negatives = [[-1, -4]] 35 | 36 | cvg = coverage.Coverage(sigma, nb_v , None, None) 37 | cvg.test(sigma, positives, negatives, nb_v) 38 | -------------------------------------------------------------------------------- /pyxai/explanations/BT_builder_regression.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/BT_builder_regression.explainer -------------------------------------------------------------------------------- /pyxai/explanations/BT_builder_simple.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/BT_builder_simple.explainer -------------------------------------------------------------------------------- /pyxai/explanations/BT_iris.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/BT_iris.explainer -------------------------------------------------------------------------------- /pyxai/explanations/BT_mnist38_tree_specific_class_3.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/BT_mnist38_tree_specific_class_3.explainer -------------------------------------------------------------------------------- /pyxai/explanations/BT_mnist38_tree_specific_class_8.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/BT_mnist38_tree_specific_class_8.explainer -------------------------------------------------------------------------------- /pyxai/explanations/BT_wine_regression.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/BT_wine_regression.explainer -------------------------------------------------------------------------------- /pyxai/explanations/DT_dexter.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/DT_dexter.explainer -------------------------------------------------------------------------------- /pyxai/explanations/DT_loan.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/DT_loan.explainer -------------------------------------------------------------------------------- /pyxai/explanations/DT_mnist38_contrastives.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/DT_mnist38_contrastives.explainer -------------------------------------------------------------------------------- /pyxai/explanations/DT_mnist38_hot_map.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/DT_mnist38_hot_map.explainer -------------------------------------------------------------------------------- /pyxai/explanations/DT_orchid.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/DT_orchid.explainer -------------------------------------------------------------------------------- /pyxai/explanations/DT_wine.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/DT_wine.explainer -------------------------------------------------------------------------------- /pyxai/explanations/RF_australian_majoritary.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/RF_australian_majoritary.explainer -------------------------------------------------------------------------------- /pyxai/explanations/RF_australian_theory.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/RF_australian_theory.explainer -------------------------------------------------------------------------------- /pyxai/explanations/RF_biodegradation.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/RF_biodegradation.explainer -------------------------------------------------------------------------------- /pyxai/explanations/RF_breast_tumor_theory.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/RF_breast_tumor_theory.explainer -------------------------------------------------------------------------------- /pyxai/explanations/RF_buba.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/RF_buba.explainer -------------------------------------------------------------------------------- /pyxai/explanations/RF_cifar_cat_minimal_majoritary.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/RF_cifar_cat_minimal_majoritary.explainer -------------------------------------------------------------------------------- /pyxai/explanations/RF_cnae.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/RF_cnae.explainer -------------------------------------------------------------------------------- /pyxai/explanations/RF_iris_majoritary_theory.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/RF_iris_majoritary_theory.explainer -------------------------------------------------------------------------------- /pyxai/explanations/RF_mnist49_minimal_majoritary.explainer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/explanations/RF_mnist49_minimal_majoritary.explainer -------------------------------------------------------------------------------- /pyxai/setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup, Extension 2 | 3 | 4 | def main(): 5 | setup(name="c_explainer", 6 | version="1.0.0", 7 | description="Python interface for the c_explainer C library function", 8 | author="", 9 | author_email="your_email@gmail.com", 10 | ext_modules=[Extension( 11 | "c_explainer", 12 | ["sources/solvers/GREEDY/src/bt_wrapper.cc", "sources/solvers/GREEDY/src/Explainer.cc", "sources/solvers/GREEDY/src/Tree.cc", 13 | "sources/solvers/GREEDY/src/Node.cc", "sources/solvers/GREEDY/src/bcp/ParserDimacs.cc", "sources/solvers/GREEDY/src/bcp/Problem.cc", 14 | "sources/solvers/GREEDY/src/bcp/ProblemTypes.cc", "sources/solvers/GREEDY/src/bcp/Propagator.cc", "sources/solvers/GREEDY/src/Rectifier.cc"], 15 | language="c++", 16 | extra_compile_args=["-std=c++11"] 17 | )]) 18 | 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /pyxai/sources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/sources/__init__.py -------------------------------------------------------------------------------- /pyxai/sources/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/sources/core/__init__.py -------------------------------------------------------------------------------- /pyxai/sources/core/explainer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/sources/core/explainer/__init__.py -------------------------------------------------------------------------------- /pyxai/sources/core/structure/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/sources/core/structure/__init__.py -------------------------------------------------------------------------------- /pyxai/sources/core/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/sources/core/tools/__init__.py -------------------------------------------------------------------------------- /pyxai/sources/core/tools/option.py: -------------------------------------------------------------------------------- 1 | class _Options: 2 | def __init__(self): 3 | self.values = tuple() # options with non-Boolean values (strings or numbers) 4 | self.flags = tuple() # Boolean options 5 | self.parameters = [] 6 | self.parameters_cursor = 0 7 | 8 | 9 | def set_values(self, *values): 10 | self.values = [value.lower() for value in values] 11 | for option in self.values: 12 | vars(self)[option] = None 13 | 14 | 15 | def set_flags(self, *flags): 16 | self.flags = [flag.lower() for flag in flags] 17 | for option in self.flags: 18 | vars(self)[option] = False 19 | 20 | 21 | def get(self, name): 22 | return vars(self)[name] 23 | 24 | 25 | def consume_parameter(self): 26 | if self.parameters_cursor < len(self.parameters): 27 | parameter = self.parameters[self.parameters_cursor] 28 | self.parameters_cursor += 1 29 | return parameter 30 | else: 31 | return None 32 | 33 | 34 | def parse(self, args): 35 | for arg in args: 36 | if arg[0] == '-': 37 | t = arg[1:].split('=', 1) 38 | t[0] = t[0].replace("-", "_") 39 | if len(t) == 1: 40 | flag = t[0].lower() 41 | if flag in self.flags: 42 | vars(self)[flag] = True 43 | assert flag not in self.values or flag == 'dataexport', "You have to specify a value for the option -" + flag 44 | else: 45 | print("Warning: the " + arg + " option is not a PyXAI option.") 46 | else: 47 | assert len(t) == 2 48 | value = t[0].lower() 49 | if value in self.values: 50 | assert len(t[1]) > 0, "The value specified for the option -" + value + " is the empty string" 51 | vars(self)[value] = t[1] 52 | else: 53 | print("Warning: the " + arg + " option is not a PyXAI option.") 54 | else: 55 | self.parameters.append(arg) 56 | 57 | 58 | Options = _Options() 59 | -------------------------------------------------------------------------------- /pyxai/sources/learning/__init__.py: -------------------------------------------------------------------------------- 1 | # from pyxai.solvers.ML import * 2 | -------------------------------------------------------------------------------- /pyxai/sources/learning/learner_information.py: -------------------------------------------------------------------------------- 1 | class LearnerInformation: 2 | def __init__(self, raw_model, training_index=None, test_index=None, group=None, metrics=None, extras=None): 3 | self.raw_model = raw_model 4 | self.training_index = training_index 5 | self.test_index = test_index 6 | self.group = group 7 | self.metrics = metrics 8 | self.extras = extras 9 | self.learner_name = None 10 | self.feature_names = None 11 | self.evaluation_method = None 12 | self.evaluation_output = None 13 | 14 | def to_dict(self): 15 | learner_dict = {} 16 | learner_dict["raw_model"] = self.raw_model 17 | learner_dict["training_index"] = self.training_index 18 | learner_dict["test_index"] = self.test_index 19 | learner_dict["group"] = self.group 20 | learner_dict["metrics"] = self.metrics 21 | learner_dict["extras"] = self.extras 22 | learner_dict["learner_name"] = self.learner_name 23 | learner_dict["feature_names"] = self.feature_names 24 | learner_dict["evaluation_method"] = self.evaluation_method 25 | learner_dict["evaluation_output"] = self.evaluation_output 26 | return learner_dict 27 | 28 | 29 | 30 | def set_learner_name(self, learner_name): 31 | self.learner_name = learner_name 32 | 33 | 34 | def set_feature_names(self, feature_names): 35 | self.feature_names = feature_names 36 | 37 | 38 | def set_evaluation_method(self, evaluation_method): 39 | self.evaluation_method = str(evaluation_method) 40 | 41 | 42 | def set_evaluation_output(self, evaluation_output): 43 | self.evaluation_output = str(evaluation_output) -------------------------------------------------------------------------------- /pyxai/sources/solvers/COMPILER/D4Solver.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import uuid 4 | 5 | D4_DIRECTORY = os.sep.join(__file__.split(os.sep)[:-1]) + os.sep 6 | D4_EXEC = D4_DIRECTORY + "d4_static" 7 | 8 | 9 | class D4Solver: 10 | 11 | def __init__(self, filenames="/tmp/heat-", _hash=str(uuid.uuid4().fields[-1])[:8]): 12 | self._hash = _hash 13 | self.filename_cnf = filenames + self._hash + ".cnf" 14 | self.filename_query = filenames + self._hash + ".query" 15 | 16 | 17 | def add_cnf(self, cnf, n_literals): 18 | file = open(self.filename_cnf, "w") 19 | file.write(f"p cnf {n_literals} {len(cnf)}\n") 20 | for clause in cnf: 21 | file.write(" ".join(str(lit) for lit in clause) + " 0\n") 22 | file.close() 23 | 24 | 25 | def add_count_model_query(self, cnf, n_literals, n_literals_limit): 26 | file = open(self.filename_query, "w") 27 | file.write(f"p cnf {n_literals} {len(cnf)}\n") 28 | file.write("m 0\n") 29 | for lit in range(1, n_literals_limit): 30 | file.write("m" + str(lit) + ' 0\n') 31 | file.close() 32 | 33 | 34 | def solve(self, time_limit=None): 35 | try: 36 | p = subprocess.run([D4_EXEC, "-m", "ddnnf-compiler", "-i", 37 | self.filename_cnf, "--query", self.filename_query], timeout=time_limit, 38 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) 39 | except subprocess.TimeoutExpired: 40 | return {1: -1} 41 | 42 | n_models = [int(line.split(" ")[1]) for line in p.stdout.split("\n") if len(line) > 0 and line[0] == "s"] 43 | return n_models 44 | 45 | def count(self, time_limit=None): 46 | try: 47 | p = subprocess.run([D4_EXEC, "-m", "ddnnf-compiler", "-i", 48 | self.filename_cnf], timeout=time_limit, 49 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) 50 | except subprocess.TimeoutExpired: 51 | return None 52 | 53 | n_models = [int(line.split(" ")[1]) for line in p.stdout.split("\n") if len(line) > 0 and line[0] == "s"] 54 | return n_models[0] 55 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/COMPILER/d4_static: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/sources/solvers/COMPILER/d4_static -------------------------------------------------------------------------------- /pyxai/sources/solvers/CSP/TSMinimalV1.py: -------------------------------------------------------------------------------- 1 | """ from pycsp3 import * 2 | 3 | protect() 4 | 5 | class MinimalV1(): 6 | 7 | 8 | def __init__(self): 9 | pass 10 | 11 | def data_formatting(self, explainer): 12 | # data_matrises is a list of matrises where each matrix represent a tree where each line is the values of a subset of the implicant. 13 | # Moreover, at the end of the line, there is the associated worst or best weight. 14 | data_matrises = [] 15 | data_domains_weights = [] 16 | data_literals_per_matrix = [] 17 | data_classes = [] 18 | for tree in explainer.boosted_trees.forest: 19 | data_classes.append(tree.target_class) 20 | variables = tree.get_variables(explainer.implicant) 21 | #print("variables:", len(variables)) 22 | subsets = list(chain.from_iterable(combinations(variables, r) for r in range(len(variables)+1))) 23 | #print("size subsets:", len(subsets)) 24 | values_per_subset = [tuple(1 if element in subset else 0 for element in variables) for subset in subsets] 25 | 26 | matrix = [] 27 | data_domains_weights_tree = [] 28 | for i, subset in enumerate(subsets): 29 | weights = explainer.compute_weights(tree, tree.root, subset) 30 | if explainer.boosted_trees.n_classes == 2: 31 | weight_value = explainer.weight_float_to_int(min(weights) if explainer.target_prediction == 1 else max(weights)) 32 | else: 33 | weight_value = explainer.weight_float_to_int(min(weights) if explainer.target_prediction == tree.target_class else max(weights)) 34 | data_domains_weights_tree.append(weight_value) 35 | line = tuple([element for element in values_per_subset[i]] + [weight_value]) 36 | matrix.append(line) 37 | data_domains_weights.append(tuple(sorted(list(set(data_domains_weights_tree))))) 38 | data_matrises.append(matrix if variables != [] else None) 39 | data_literals_per_matrix.append([explainer.implicant.index(v) for v in variables]) 40 | return data_matrises, data_domains_weights, data_literals_per_matrix, data_classes 41 | 42 | 43 | def create_model_minimal_abductive_BT(self, implicant, data_matrises, data_domains_weights, data_literals_per_matrix, data_classes, prediction): 44 | 45 | n_trees = len(data_matrises) 46 | 47 | # Say if a literal of the implicant is enabled or not 48 | literals = VarArray(size=len(implicant), dom={0, 1}) 49 | 50 | # The weight of each tree according to the variables 'literals' 51 | weights = VarArray(size=[n_trees], dom=lambda x: data_domains_weights[x]) 52 | 53 | #exit(0) 54 | satisfy( 55 | # Constrain table fixing the weight of each tree according to the variable 'literal' 56 | ([literals[lit] for lit in data_literals_per_matrix[id_tree]]+[weights[id_tree]] in data_matrises[id_tree] for id_tree in range(n_trees) if data_matrises[id_tree] is not None), 57 | 58 | # Take only these that classify always correctly the implicant 59 | [Sum(weights[i] for i in range(n_trees) if data_classes[i] == prediction) > Sum(weights[i] for i in range(n_trees) if data_classes[i] == other_class) for other_class in set(data_classes) if other_class != prediction] 60 | ) 61 | 62 | # The goal is to have the most little subset :) 63 | minimize(Sum(literals)) 64 | 65 | 66 | def solve(self): 67 | ace = solver(ACE) 68 | ace.setting("-ale=4 -p=SAC3 -so") 69 | instance = compile() 70 | result = ace.solve(instance, verbose=True) 71 | print(result) 72 | print(solution().values) 73 | if result == OPTIMUM: 74 | return [value for i, value in enumerate(solution().values) if 'literals' in solution().variables[i].id] 75 | return None """ 76 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/ENCORE/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/sources/solvers/ENCORE/__init__.py -------------------------------------------------------------------------------- /pyxai/sources/solvers/GRAPH/TreeDecomposition.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import os 3 | 4 | 5 | class TreeDecomposition(): 6 | 7 | def __init__(self): 8 | pass 9 | 10 | 11 | def create_instance(self, BTs): 12 | # BTs: the model 13 | trees = BTs.forest 14 | 15 | variables = set() 16 | 17 | combinations = set() 18 | for tree in trees: 19 | variables_set = BTs.get_set_of_variables(tree, tree.root) 20 | variables = variables.union(variables_set) 21 | combinations = combinations.union(set(itertools.combinations(variables_set, 2))) 22 | n_vertices = len(variables) 23 | n_edges = len(combinations) 24 | # print(combinations) 25 | # print(variables) 26 | 27 | instance_lines = [] 28 | instance_lines.append("p tw " + str(n_vertices) + " " + str(n_edges) + "\n") 29 | for edge in combinations: 30 | instance_lines.append(str(edge[0]) + " " + str(edge[1]) + "\n") 31 | 32 | self.graph_file = "graph.txt" 33 | if os.path.exists(self.graph_file): 34 | os.remove(self.graph_file) 35 | f = open(self.graph_file, "a") 36 | for line in instance_lines: 37 | f.write(line) 38 | f.close() 39 | 40 | 41 | def solve(self): 42 | 43 | return None 44 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/GREEDY/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.4...3.18) 2 | project(cmake_example) 3 | 4 | 5 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}) 6 | 7 | file(GLOB_RECURSE Greedy_SOURCES 8 | ${PROJECT_SOURCE_DIR}/*.cc 9 | ${PROJECT_SOURCE_DIR}/bcp*.cc) 10 | 11 | file(GLOB_RECURSE Greedy_HEADERS 12 | ${PROJECT_SOURCE_DIR}/*.h 13 | ${PROJECT_SOURCE_DIR}/bcp*.h) 14 | 15 | file(GLOB_RECURSE Greedy_HEADERS "./*.h") 16 | 17 | include_directories( 18 | ${Greedy_HEADERS} 19 | ) 20 | #add_library(MyExample STATIC ${Greedy_SOURCES}) 21 | 22 | add_executable(MyExample ${Greedy_SOURCES}) 23 | # EXAMPLE_VERSION_INFO is defined by setup.py and passed into the C++ code as a 24 | # define (VERSION_INFO) here. 25 | 26 | #target_compile_definitions(majoritary_bt PRIVATE VERSION_INFO=${EXAMPLE_VERSION_INFO}) -------------------------------------------------------------------------------- /pyxai/sources/solvers/GREEDY/src/Explainer.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by audemard on 22/04/2022. 3 | // 4 | 5 | #ifndef CPP_CODE_EXPLAINER_H 6 | #define CPP_CODE_EXPLAINER_H 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "Tree.h" 14 | #include "utils/TimerHelper.h" 15 | #include "bcp/Propagator.h" 16 | #include "bcp/Problem.h" 17 | namespace pyxai { 18 | class Explainer { 19 | public: 20 | int n_classes; 21 | Type _type; 22 | int n_iterations; 23 | int time_limit; //in seconds 24 | int try_to_remove; 25 | std::vector excluded_features; 26 | Propagator *theory_propagator = nullptr; 27 | double lower_bound; // Useful for regression only 28 | double upper_bound; 29 | double base_score; 30 | 31 | Explainer(int _n_classes, Type t) : n_classes(_n_classes), _type(t), n_iterations(50), time_limit(0), base_score(0.5) {} 32 | 33 | 34 | void addTree(PyObject *tree_obj); 35 | std::vector trees; 36 | bool compute_reason_conditions(std::vector &instance, std::vector &weights, int prediction, std::vector &reason, long seed, double theta); 37 | void initializeBeforeOneRun(std::vector & polarity_instance, std::vector&active_litd, int prediction); 38 | void propagateActiveLits( std::vector &order, std::vector &polarity_instance, std::vector &active_lits); 39 | 40 | bool compute_reason_features(std::vector &instance, std::vector &features, int prediction, std::vector &reason, double theta); 41 | 42 | bool is_implicant(std::vector &instance, std::vector &active_lits, unsigned int prediction, double theta); 43 | 44 | bool is_implicant_BT(std::vector &instance, std::vector &active_lits, unsigned int prediction, double theta); 45 | bool is_implicant_RF(std::vector &instance, std::vector &active_lits, unsigned int prediction); 46 | bool is_implicant_regression_BT(std::vector &instance, std::vector &active_lits, unsigned int prediction); 47 | 48 | inline void set_n_iterations(int _n_iterations){n_iterations = _n_iterations;} 49 | inline void set_time_limit(int _time_limit){time_limit = _time_limit;} 50 | 51 | 52 | inline bool is_specific(int l) {return std::find(excluded_features.begin(), excluded_features.end(), l) == excluded_features.end();} 53 | inline void set_interval(double lb, double ub) { 54 | lower_bound = lb; 55 | upper_bound = ub; 56 | } 57 | 58 | }; 59 | } 60 | 61 | 62 | #endif //CPP_CODE_EXPLAINERBT_H 63 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/GREEDY/src/Rectifier.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Created by audemard on 22/04/2022. 3 | // 4 | 5 | #include "Rectifier.h" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "Tree.h" 15 | #include "bcp/ProblemTypes.h" 16 | 17 | void pyxai::Rectifier::addTree(PyObject *tree_obj) { 18 | Tree *tree = new Tree(tree_obj, pyxai::Classifier_RF); 19 | trees.push_back(tree); 20 | } 21 | 22 | void pyxai::Rectifier::improvedRectification(PyObject *conditions_obj, int _label){ 23 | Py_ssize_t size_conditions_obj = PyTuple_Size(conditions_obj); 24 | //std::cout << "size_condition_tuple:" << size_conditions_obj << std::endl; 25 | conditions.clear(); 26 | 27 | for (unsigned int i = 0; i < size_conditions_obj; i++){ 28 | PyObject *literal_obj = PyTuple_GetItem(conditions_obj, i); 29 | if (!PyLong_Check(literal_obj)) { 30 | PyErr_Format(PyExc_TypeError, 31 | "The element of the tuple must be a integer representing a literal !"); 32 | return; 33 | } 34 | conditions.push_back(PyLong_AsLong(literal_obj)); 35 | } 36 | 37 | label = _label; 38 | //std::cout << "label:" << label << std::endl; 39 | for (Tree *tree: trees) {tree->improvedRectification(&conditions, label);} 40 | 41 | } 42 | 43 | void pyxai::Rectifier::addDecisionRule(PyObject *tree_obj) { 44 | Tree *tree = new Tree(tree_obj, pyxai::Classifier_RF); 45 | decision_rules.push_back(tree); 46 | } 47 | 48 | int pyxai::Rectifier::nNodes() { 49 | int sum = 0; 50 | for (Tree *tree: trees) {sum = sum + tree->nNodes();} 51 | return sum; 52 | } 53 | 54 | void pyxai::Rectifier::negatingDecisionRules() { 55 | for (Tree *decision_rule: decision_rules) {decision_rule->negating_tree();} 56 | } 57 | 58 | void pyxai::Rectifier::free(){ 59 | for (Tree *tree: trees) { 60 | tree->free(); 61 | delete tree; 62 | } 63 | for (Tree *decision_rule: decision_rules) { 64 | decision_rule->free(); 65 | delete decision_rule; 66 | } 67 | 68 | decision_rules.clear(); 69 | trees.clear(); 70 | } 71 | 72 | void pyxai::Rectifier::simplifyRedundant(){ 73 | for (Tree *tree: trees) {tree->simplifyRedundant();} 74 | } 75 | 76 | void pyxai::Rectifier::disjointTreesDecisionRule() { 77 | for (unsigned int i = 0; i < trees.size(); i++){ 78 | trees[i]->disjointTreeDecisionRule(decision_rules[i]); 79 | } 80 | } 81 | 82 | void pyxai::Rectifier::concatenateTreesDecisionRule() { 83 | for (unsigned int i = 0; i < trees.size(); i++){ 84 | trees[i]->concatenateTreeDecisionRule(decision_rules[i]); 85 | } 86 | } 87 | 88 | void pyxai::Rectifier::simplifyTheory() { 89 | for (Tree *tree: trees) tree->simplifyTheory(); 90 | } 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/GREEDY/src/Rectifier.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by audemard on 22/04/2022. 3 | // 4 | 5 | #ifndef CPP_CODE_RECTIFIER_H 6 | #define CPP_CODE_RECTIFIER_H 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "Tree.h" 14 | #include "utils/TimerHelper.h" 15 | #include "bcp/Propagator.h" 16 | #include "bcp/Problem.h" 17 | namespace pyxai { 18 | class Rectifier { 19 | public: 20 | std::vector trees; 21 | std::vector decision_rules; 22 | std::vector conditions; 23 | int label; 24 | 25 | Propagator *theory_propagator = nullptr; 26 | 27 | Rectifier(): trees(), decision_rules(), conditions(), label(0){}; 28 | 29 | void addTree(PyObject *tree_obj); 30 | void addDecisionRule(PyObject *tree_obj); 31 | 32 | void improvedRectification(PyObject *conditions_obj, int _label); 33 | 34 | void negatingDecisionRules(); 35 | void disjointTreesDecisionRule(); 36 | void concatenateTreesDecisionRule(); 37 | 38 | void simplifyTheory(); 39 | void simplifyRedundant(); 40 | 41 | int nNodes(); 42 | 43 | void free(); 44 | }; 45 | } 46 | 47 | 48 | #endif //CPP_CODE_RECTIFIER_H 49 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/GREEDY/src/Tree.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by audemard on 22/04/2022. 3 | // 4 | 5 | #ifndef CPP_CODE_TREE_H 6 | #define CPP_CODE_TREE_H 7 | 8 | #include "Node.h" 9 | #include "bcp/Propagator.h" 10 | #include "constants.h" 11 | #include 12 | 13 | #if (__sun && __SVR4) 14 | /* libnet should be using the standard type names, but in the short term 15 | * define our non-standard type names in terms of the standard names. 16 | */ 17 | #include 18 | typedef uint8_t u_int8_t; 19 | typedef uint16_t u_int16_t; 20 | typedef uint32_t u_int32_t; 21 | typedef uint64_t u_int64_t; 22 | typedef uint64_t u_int64_t; 23 | #endif 24 | 25 | namespace pyxai { 26 | enum Kind_of_Tree_RF { DEFINITIVELY_WRONG, GOOD, CURRENTLY_WRONG}; 27 | class Node; 28 | 29 | class Tree { 30 | public : 31 | Type _type; 32 | unsigned int n_classes; 33 | unsigned int target_class; 34 | unsigned int *memory = nullptr; 35 | Node *root = nullptr; 36 | std::vector all_nodes; 37 | Kind_of_Tree_RF status; // Useful only with Classifier_RF : this tree hasn't the good class 38 | std::vector used_to_explain; // related to instance: true if the lit is used to explain the tree 39 | std::vector used_lits; 40 | Propagator *propagator = nullptr; 41 | std::set to_delete; 42 | 43 | // Variables used to stored the comutation value during common is_impicant function 44 | // FOR Classifier_BT 45 | bool get_min; 46 | double current_weight; 47 | bool firstLeaf; 48 | double current_min_weight, current_max_weight; // For regression BT 49 | 50 | std::set reachable_classes; // FOR Multiclasses Classifier_RF 51 | 52 | 53 | Tree(PyObject *tree_obj, Type _t): _type(_t) { 54 | root = parse(tree_obj, _t); 55 | } 56 | 57 | void display(Type _type); 58 | Node* parse(PyObject *tree_obj, Type _type); 59 | Node* parse_recurrence(PyObject *tree_obj, Type _type); 60 | int nb_nodes(); 61 | 62 | PyObject* toTuple(); 63 | 64 | void initialize_BT(std::vector &instance, bool get_min); 65 | 66 | 67 | bool is_implicant(std::vector &instance, std::vector &active_lits, int prediction); 68 | 69 | int nNodes(); 70 | 71 | 72 | void initialize_RF(std::vector &instance, std::vector &active_lits, int prediction); 73 | 74 | bool equalTree(Node* node1, Node* node2); 75 | 76 | void negating_tree(); 77 | void concatenateTreeDecisionRule(Tree* decision_rule); 78 | void disjointTreeDecisionRule(Tree* decision_rule); 79 | 80 | void improvedRectification(std::vector* conditions, int& label); 81 | void _improvedRectification(Node* node, Node* parent, int come_from, std::vector* stack, std::vector* appeared_conditions, std::vector* in_conditions, std::vector* conditions, int& label); 82 | 83 | void simplifyTheory(); 84 | void free(); 85 | void simplifyRedundant(); 86 | bool _simplifyRedundant(Node* root, Node* node, std::vector* path, int come_from, Node* previous_node, Node* previous_previous_node); 87 | 88 | Node* _simplifyTheory(Node* node, std::vector* stack, Node* parent, int come_from, Node* root); 89 | std::vector* isNodeConsistent(Node* node, std::vector* stack); 90 | 91 | void update_used_lits() { 92 | for(int i : used_lits) 93 | used_to_explain[i] = true; 94 | } 95 | }; 96 | } 97 | #endif //CPP_CODE_TREE_H 98 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/GREEDY/src/bcp/FactoryException.h: -------------------------------------------------------------------------------- 1 | /** 2 | * rfx 3 | * Copyright (C) 2021 Lagniez Jean-Marie 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | #pragma once 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | namespace pyxai { 26 | class FactoryException : public std::exception { 27 | private: 28 | std::string m_error_message; 29 | const char *m_file; 30 | int m_line; 31 | 32 | public: 33 | FactoryException(const char *msg, const char *file_, int line_) 34 | : m_file(file_), m_line(line_) { 35 | std::ostringstream o; 36 | o << m_file << ":" << m_line << ": " << msg; 37 | m_error_message = o.str(); 38 | } // constructor 39 | 40 | /** 41 | Returns a pointer to the (constant) error description. 42 | 43 | \return A pointer to a const char*. 44 | */ 45 | virtual const char *what() const throw() { return m_error_message.c_str(); } 46 | }; 47 | } // namespace rfx 48 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/GREEDY/src/bcp/ParserDimacs.h: -------------------------------------------------------------------------------- 1 | /** 2 | * rfx 3 | * Copyright (C) 2021 Lagniez Jean-Marie 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | #pragma once 19 | 20 | #include 21 | #include 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #include "BufferRead.h" 30 | #include "Problem.h" 31 | #include "ProblemTypes.h" 32 | 33 | namespace pyxai { 34 | class ParserDimacs { 35 | private: 36 | int parse_DIMACS_main(BufferRead &in, Problem *problemManager); 37 | 38 | void readListIntTerminatedByZero(BufferRead &in, std::vector &list); 39 | void parseWeightedLit(BufferRead &in, std::vector &weightLit); 40 | 41 | public: 42 | int parse_DIMACS(std::string input_stream, Problem *problemManager); 43 | }; 44 | } // namespace rfx 45 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/GREEDY/src/bcp/Problem.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * rfx 3 | * Copyright (C) 2021 Lagniez Jean-Marie 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include "Problem.h" 20 | 21 | #include "ParserDimacs.h" 22 | #include "Problem.h" 23 | 24 | namespace pyxai { 25 | 26 | Problem::Problem(const std::string &nameFile, std::ostream &out, bool verbose) { 27 | if (verbose) out << "c [rfx PROBLEM] Constructor from file.\n"; 28 | ParserDimacs parser; 29 | if (verbose) out << "c [rfx PROBLEM] Call the parser ... " << std::flush; 30 | m_nbVar = parser.parse_DIMACS(nameFile, this); 31 | if (verbose) out << "done\n"; 32 | if (verbose) displayStat(out, "c [rfx PARSER] "); 33 | } // constructor 34 | 35 | Problem::Problem() { m_nbVar = 0; } // constructor 36 | 37 | Problem::Problem(Problem &problem, std::ostream &out, bool verbose) { 38 | if (verbose) out << "c [rfx PROBLEM] Constructor from problem.\n"; 39 | m_nbVar = problem.getNbVar(); 40 | m_clauses = problem.getClauses(); 41 | if (verbose) displayStat(out, "c [PARSER] "); 42 | } // constructor 43 | 44 | Problem::Problem(std::vector> &clauses, unsigned nbVar, 45 | std::ostream &out, bool verbose) { 46 | if (verbose) out << "c [rfx PROBLEM] Constructor from clauses.\n"; 47 | m_nbVar = nbVar; 48 | m_clauses = clauses; 49 | if (verbose) displayStat(out, "c [rfx PARSER] "); 50 | } // constructor 51 | 52 | Problem *Problem::getUnsatProblem() { 53 | Problem *ret = new Problem(); 54 | ret->setNbVar(m_nbVar); 55 | 56 | std::vector cl; 57 | Lit l = Lit::makeLit(1, false); 58 | 59 | cl.push_back(l); 60 | ret->getClauses().push_back(cl); 61 | 62 | cl[0] = l.neg(); 63 | ret->getClauses().push_back(cl); 64 | 65 | return ret; 66 | } // getUnsatProblem 67 | 68 | void Problem::display(std::ostream &out) { 69 | out << "p cnf " << m_nbVar << " " << m_clauses.size() << "\n"; 70 | 71 | // print the comments 72 | for (auto &comment : m_comments) out << comment; 73 | 74 | // print the clauses. 75 | for (auto cl : m_clauses) { 76 | for (auto &l : cl) out << l << " "; 77 | out << "0\n"; 78 | } 79 | } // diplay 80 | 81 | void Problem::displayStat(std::ostream &out, std::string startLine) { 82 | unsigned nbLits = 0; 83 | unsigned nbBin = 0; 84 | unsigned nbTer = 0; 85 | unsigned nbMoreThree = 0; 86 | 87 | for (auto &c : m_clauses) { 88 | nbLits += c.size(); 89 | if (c.size() == 2) nbBin++; 90 | if (c.size() == 3) nbTer++; 91 | if (c.size() > 3) nbMoreThree++; 92 | } 93 | 94 | out << startLine << "Number of variables: " << m_nbVar << "\n"; 95 | out << startLine << "Number of clauses: " << m_clauses.size() << "\n"; 96 | out << startLine << "Number of binary clauses: " << nbBin << "\n"; 97 | out << startLine << "Number of ternary clauses: " << nbTer << "\n"; 98 | out << startLine << "Number of clauses larger than 3: " << nbMoreThree 99 | << "\n"; 100 | out << startLine << "Number of literals: " << nbLits << "\n"; 101 | } // displaystat 102 | 103 | } // namespace rfx 104 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/GREEDY/src/bcp/Problem.h: -------------------------------------------------------------------------------- 1 | /** 2 | * rfx 3 | * Copyright (C) 2021 Lagniez Jean-Marie 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | #pragma once 19 | 20 | #include 21 | 22 | #include "./ProblemTypes.h" 23 | 24 | namespace pyxai { 25 | class Problem { 26 | private: 27 | std::vector> m_clauses; 28 | std::vector m_comments; 29 | unsigned m_nbVar; 30 | 31 | public: 32 | /** 33 | * @brief Construct a new Problem object 34 | */ 35 | Problem(); 36 | 37 | /** 38 | * @brief Construct a new Problem object 39 | * 40 | * @param nameFile is the file where we parse the CNF. 41 | * @param out is the stream where the logs are printed out. 42 | * @param verbose is set to true if we want to print out information. 43 | */ 44 | Problem(const std::string &nameFile, std::ostream &out, bool verbose = false); 45 | 46 | /** 47 | * @brief Construct a new Problem object 48 | * 49 | * @param clause is the set of CNF clauses. 50 | * @param nbVar is the number of CNF variables. 51 | * @param out is the stream where are printed out the information. 52 | * @param verbose is set to true if we want to print out information. 53 | */ 54 | Problem(std::vector> &clauses, unsigned nbVar, 55 | std::ostream &out, bool verbose = false); 56 | 57 | /** 58 | * @brief Construct a new Problem object 59 | * 60 | * @param problem is the problem we want to copy. 61 | * @param out is the stream where the logs are printed out. 62 | * @param verbose is set to true if we want to print out information. 63 | */ 64 | Problem(Problem &problem, std::ostream &out, bool verbose = false); 65 | 66 | inline unsigned getNbVar() { return m_nbVar; } 67 | inline std::vector> &getClauses() { return m_clauses; } 68 | inline std::vector &getComments() { return m_comments; } 69 | inline void setNbVar(unsigned nbVar) { m_nbVar = nbVar; } 70 | inline void setClauses(std::vector> &clauses) { 71 | m_clauses = clauses; 72 | } 73 | 74 | /** 75 | * @brief Get the Unsat ProblemManager object. 76 | * 77 | * @return an unsatisfiable problem. 78 | */ 79 | Problem *getUnsatProblem(); 80 | 81 | /** 82 | Display the problem. 83 | 84 | @param[out] out, the stream where the messages are redirected. 85 | */ 86 | void display(std::ostream &out); 87 | 88 | /** 89 | Print out some statistic about the problem. Each line will start with the 90 | string startLine given in parameter. 91 | 92 | @param[in] out, the stream where the messages are redirected. 93 | @param[in] startLine, each line will start with this string. 94 | */ 95 | void displayStat(std::ostream &out, std::string startLine); 96 | }; 97 | } // namespace rfx 98 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/GREEDY/src/bcp/ProblemTypes.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * bipe 3 | * Copyright (C) 2021 Lagniez Jean-Marie 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #include "ProblemTypes.h" 20 | 21 | namespace pyxai { 22 | /** 23 | Redefinition of the toString method. 24 | */ 25 | std::ostream &operator<<(std::ostream &os, Lit l) { 26 | os << (l.sign() ? -l.var() : l.var()); 27 | return os; 28 | } // redefinition of << 29 | 30 | } // namespace rfx 31 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/GREEDY/src/bcp/ProblemTypes.h: -------------------------------------------------------------------------------- 1 | /** 2 | * rfx 3 | * Copyright (C) 2021 Lagniez Jean-Marie 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published 7 | * by the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | #pragma once 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | namespace pyxai { 26 | 27 | typedef int Var; 28 | typedef uint8_t lbool; 29 | 30 | const Var var_Undef = -1; 31 | const lbool l_True = 0; 32 | const lbool l_False = 1; 33 | const lbool l_Undef = 2; 34 | 35 | struct Lit { 36 | int m_x; 37 | 38 | inline bool sign() const { return m_x & 1; } 39 | inline Var var() const { return m_x >> 1; } 40 | inline Lit neg() const { return {m_x ^ 1}; } 41 | inline unsigned intern() const { return m_x; } 42 | inline int human() const { return (m_x & 1) ? -var() : var(); } 43 | 44 | bool operator==(Lit p) const { return m_x == p.m_x; } 45 | bool operator!=(Lit p) const { return m_x != p.m_x; } 46 | bool operator<(Lit p) const { 47 | return m_x < p.m_x; 48 | } // '<' makes p, ~p adjacent in the ordering. 49 | 50 | friend Lit operator~(Lit p); 51 | friend std::ostream &operator<<(std::ostream &os, Lit l); 52 | 53 | static inline Lit makeLit(Var v, bool sign) { return {(v << 1) + sign}; } 54 | static inline Lit makeLitFalse(Var v) { return {(v << 1) + 1}; } 55 | static inline Lit makeLitTrue(Var v) { return {v << 1}; } 56 | }; 57 | 58 | const Lit lit_Undef = {-2}; // }- Useful special constants. 59 | const Lit lit_Error = {-1}; // } 60 | 61 | inline void showListLit(std::ostream &out, std::vector &v) { 62 | for (auto &l : v) out << l << " "; 63 | } // showListLit 64 | 65 | inline Lit operator~(Lit p) { return {p.m_x ^ 1}; } 66 | 67 | struct Clause { 68 | unsigned size; 69 | Lit data[0]; 70 | 71 | Lit &operator[](std::size_t idx) { return data[idx]; } 72 | 73 | inline void swap(unsigned i, unsigned j) { 74 | assert(i < size && j < size); 75 | Lit tmp = data[i]; 76 | data[i] = data[j]; 77 | data[j] = tmp; 78 | } 79 | 80 | inline void display(std::ostream &out) { 81 | for (unsigned i = 0; i < size; i++) out << data[i] << " "; 82 | std::cout << "\n"; 83 | } 84 | }; 85 | 86 | } // namespace rfx 87 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/GREEDY/src/constants.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by audemard on 17/03/23. 3 | // 4 | 5 | #ifndef CMAKE_EXAMPLE_CONSTANTS_H 6 | #define CMAKE_EXAMPLE_CONSTANTS_H 7 | namespace pyxai { 8 | typedef enum tdtype {Classifier_BT, Classifier_RF, Regression_BT} Type; 9 | } 10 | #endif //CMAKE_EXAMPLE_CONSTANTS_H 11 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/GREEDY/src/utils/TimerHelper.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef miniMOLS_TimerHelper_h 3 | #define miniMOLS_TimerHelper_h 4 | 5 | 6 | 7 | //It is __GLIBC__ instead of __linux__ 8 | #if defined(__GLIBC__) 9 | #include 10 | #endif 11 | 12 | #if defined(_MSC_VER) || defined(__MINGW32__) 13 | #include 14 | #else 15 | #include 16 | #include 17 | #include 18 | #include 19 | #endif 20 | 21 | namespace pyxai { 22 | static double initRealTime = 0; 23 | static double initCpuTime = 0; 24 | static bool isInitialized = false; 25 | 26 | namespace TimerHelper { 27 | #if defined(_WIN32) 28 | // MSVC defines this in winsock2.h!? 29 | #include < time.h > 30 | #include < windows.h > 31 | 32 | #if defined(_MSC_VER) || defined(_MSC_EXTENSIONS) 33 | #define DELTA_EPOCH_IN_MICROSECS 11644473600000000Ui64 34 | #else 35 | #define DELTA_EPOCH_IN_MICROSECS 11644473600000000ULL 36 | #endif 37 | typedef struct timeval_win { 38 | long tv_sec; 39 | long tv_usec; 40 | } timeval_win; 41 | 42 | inline int gettimeofday_win(timeval_win * tp) 43 | { 44 | // Note: some broken versions only have 8 trailing zero's, the correct epoch has 9 trailing zero's 45 | // This magic number is the number of 100 nanosecond intervals since January 1, 1601 (UTC) 46 | // until 00:00:00 January 1, 1970 47 | static const uint64_t EPOCH = ((uint64_t) 116444736000000000ULL); 48 | 49 | SYSTEMTIME system_time; 50 | FILETIME file_time; 51 | uint64_t time; 52 | 53 | GetSystemTime( &system_time ); 54 | SystemTimeToFileTime( &system_time, &file_time ); 55 | time = ((uint64_t)file_time.dwLowDateTime ) ; 56 | time += ((uint64_t)file_time.dwHighDateTime) << 32; 57 | 58 | tp->tv_sec = (long) ((time - EPOCH) / 10000000L); 59 | tp->tv_usec = (long) (system_time.wMilliseconds * 1000); 60 | return 0; 61 | } 62 | #endif // _WIN32 63 | 64 | inline double cpuTime() { 65 | if(!isInitialized){ 66 | std::cout << "Warning: initializeTime() has not been called before !" << std::endl; 67 | } 68 | #if defined(_MSC_VER) || defined(__MINGW32__) 69 | return ((double)clock() / CLOCKS_PER_SEC) - initCpuTime; 70 | #else 71 | struct rusage ru; 72 | getrusage(RUSAGE_SELF, &ru); 73 | if (initCpuTime != 0) 74 | return ((double)ru.ru_utime.tv_sec + (double)ru.ru_utime.tv_usec / 1000000) - initCpuTime; 75 | 76 | return (double)ru.ru_utime.tv_sec + (double)ru.ru_utime.tv_usec / 1000000; 77 | #endif 78 | } 79 | 80 | inline double realTime() { 81 | if(!isInitialized){ 82 | std::cout << "Warning: initializeTime() has not been called before !" << std::endl; 83 | } 84 | #if defined(_MSC_VER) || defined(__MINGW32__) 85 | struct timeval_win tv; 86 | gettimeofday_win(&tv); 87 | #else 88 | struct timeval tv; 89 | gettimeofday(&tv, NULL); 90 | #endif 91 | if (initRealTime != 0) 92 | return ((double)tv.tv_sec + (double) tv.tv_usec / 1000000) - initRealTime; 93 | 94 | return (double)tv.tv_sec + (double) tv.tv_usec / 1000000; 95 | } 96 | 97 | inline void initializeTime(){ 98 | initRealTime = 0; 99 | initCpuTime = 0; 100 | isInitialized = true; 101 | initRealTime = realTime(); 102 | initCpuTime = cpuTime(); 103 | } 104 | 105 | 106 | 107 | } 108 | } 109 | #endif -------------------------------------------------------------------------------- /pyxai/sources/solvers/MAXSAT/MAXSATSolver.py: -------------------------------------------------------------------------------- 1 | from pysat.formula import WCNF 2 | 3 | 4 | class MAXSATSolver: 5 | def __init__(self): 6 | self.WCNF = WCNF() 7 | 8 | 9 | def add_soft_clause(self, clause, weight): 10 | self.WCNF.append(clause, weight=weight) 11 | 12 | 13 | def add_hard_clause(self, clause): 14 | self.WCNF.append(clause) 15 | 16 | def add_hard_clauses(self, clauses): 17 | self.WCNF.extend(clauses) 18 | 19 | def solve(self, *, time_limit=0): 20 | raise NotImplementedError 21 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/MAXSAT/OPENWBOSolver.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import time 4 | import uuid 5 | 6 | from pyxai import Explainer 7 | from pyxai.sources.core.tools.utils import get_os 8 | from pyxai.sources.solvers.MAXSAT.MAXSATSolver import MAXSATSolver 9 | 10 | OPENWBO_DIRECTORY = os.sep.join(__file__.split(os.sep)[:-1]) + os.sep 11 | OPENWBO_EXEC = OPENWBO_DIRECTORY + "openwbo-" + get_os() 12 | 13 | 14 | class OPENWBOSolver(MAXSATSolver): 15 | def __init__(self, _hash=str(uuid.uuid4().fields[-1])[:8]): 16 | super().__init__() 17 | self._hash = _hash 18 | if not os.path.exists("tmp"): 19 | os.mkdir("tmp") 20 | self.filename_wcnf = "tmp" + os.sep + "wbo-" + self._hash + ".wcnf" 21 | 22 | 23 | def solve(self, *, time_limit=0): 24 | self.WCNF.to_file(self.filename_wcnf) 25 | 26 | params = [OPENWBO_EXEC] 27 | time_used = -time.time() 28 | if time_limit != 0: 29 | params += [f"-cpu-lim={time_limit}"] 30 | params += [self.filename_wcnf] 31 | p = subprocess.run(params, timeout=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) 32 | time_used += time.time() 33 | output_str = [line.split(" ") for line in p.stdout.split(os.linesep) if len(line) > 0 and line[0] == "v"] 34 | if len(output_str) == 0: 35 | return p.stderr, None, time_used 36 | 37 | status = [line.split(" ")[1] for line in p.stdout.split(os.linesep) if len(line) > 0 and line[0] == "s"][0] 38 | model = [int(lit) for lit in output_str[0] if lit != 'v' and lit != ''] 39 | return status, model, Explainer.TIMEOUT if status == "SATISFIABLE" else time_used 40 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/MAXSAT/RC2solver.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from pysat.examples.rc2 import RC2 4 | from pysat.pb import * 5 | 6 | from pyxai.sources.solvers.MAXSAT.MAXSATSolver import MAXSATSolver 7 | 8 | 9 | # from pysat.pb import PBEnc 10 | 11 | class RC2MAXSATsolver(MAXSATSolver): 12 | 13 | def __init__(self): 14 | super().__init__() 15 | 16 | 17 | def add_soft_clauses_implicant(self, implicant): 18 | for lit in implicant: 19 | self.WCNF.append([-lit], weight=1) 20 | 21 | 22 | def add_hard_clauses(self, tree_CNF, implicant=None): 23 | for clause in tree_CNF: # hard 24 | if implicant is not None: 25 | new_clause = [lit for lit in clause if lit in implicant] 26 | assert new_clause != [], "This clause cannot be empty" 27 | self.WCNF.append(new_clause) 28 | else: 29 | self.WCNF.append(clause) 30 | 31 | 32 | def add_atmost(self, implicant, sufficient_reason): 33 | self.WCNF.extend( 34 | PBEnc.atmost(lits=implicant, top_id=self.WCNF.nv + 1, bound=len(sufficient_reason)).clauses) 35 | 36 | 37 | def add_clause(self, clause): 38 | self.WCNF.append(clause) 39 | 40 | 41 | def solve_implicant(self, implicant): 42 | result = RC2(self.WCNF).compute() 43 | return None if result is None else [lit for lit in result if lit in implicant] 44 | 45 | 46 | def solve(self, *, time_limit=0): 47 | time_used = - time.time() 48 | result = RC2(self.WCNF).compute() 49 | time_used += time.time() 50 | return (None, None, None) if result is None else ("OPTIMUM FOUND", [lit for lit in result], time_used) 51 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/MAXSAT/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/sources/solvers/MAXSAT/__init__.py -------------------------------------------------------------------------------- /pyxai/sources/solvers/MAXSAT/openwbo-darwin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/sources/solvers/MAXSAT/openwbo-darwin -------------------------------------------------------------------------------- /pyxai/sources/solvers/MAXSAT/openwbo-linux: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/sources/solvers/MAXSAT/openwbo-linux -------------------------------------------------------------------------------- /pyxai/sources/solvers/MAXSAT/openwbo-windows: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/sources/solvers/MAXSAT/openwbo-windows -------------------------------------------------------------------------------- /pyxai/sources/solvers/MIP/Range.py: -------------------------------------------------------------------------------- 1 | from ortools.linear_solver import pywraplp 2 | from .help_functions import * 3 | 4 | 5 | class Range: 6 | def __init__(self): 7 | pass 8 | 9 | 10 | def create_model_and_solve(self, explainer, theory, partial_binary_representation, minimum, time_limit): 11 | forest = explainer._boosted_trees.forest 12 | bin_len = len(explainer.binary_representation) 13 | solver = pywraplp.Solver.CreateSolver("SCIP") 14 | features_to_bin = explainer._boosted_trees.get_id_binaries() 15 | 16 | if time_limit is not None: 17 | solver.SetTimeLimit(time_limit * 1000) # time limit in milisecond 18 | 19 | # Model variables 20 | instance = instance_variables(solver, bin_len) # The instance 21 | active_leaves = active_leaves_variables(solver, forest) # active leaves 22 | 23 | 24 | # Constraints related to tree structure 25 | tree_structure_constraints(explainer, solver, active_leaves, instance) 26 | 27 | # Constraints related to theory 28 | theory_constraints(solver, instance, theory) 29 | 30 | # Fix partial binary representation 31 | for lit in partial_binary_representation: 32 | v = 1 if lit > 0 else 0 33 | constraint = solver.RowConstraint(v, v) 34 | constraint.SetCoefficient(instance[abs(lit) - 1], 1) 35 | 36 | # set the objective 37 | objective = solver.Objective() 38 | 39 | for j, tree in enumerate(forest): 40 | for i, leave in enumerate(tree.get_leaves()): 41 | objective.SetCoefficient(active_leaves[j][i], leave.value) 42 | 43 | if minimum: 44 | objective.SetMinimization() 45 | else: 46 | objective.SetMaximization() 47 | 48 | solver.Solve() 49 | return solver.Objective().Value() -------------------------------------------------------------------------------- /pyxai/sources/solvers/MIP/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/sources/solvers/MIP/__init__.py -------------------------------------------------------------------------------- /pyxai/sources/solvers/MIP/help_functions.py: -------------------------------------------------------------------------------- 1 | from pyxai.sources.core.structure.type import TypeLeaf 2 | from pyxai.sources.core.structure.type import TypeLeaf 3 | 4 | 5 | def instance_variables(solver, bin_len): 6 | return [solver.BoolVar(f"x[{i}]") for i in range(bin_len)] 7 | 8 | def active_leaves_variables(solver, forest): 9 | active_leaves = [] 10 | for j, tree in enumerate(forest): 11 | active_leaves.append([solver.BoolVar(f"y[{j}][{i}]") for i in range(len(tree.get_leaves()))]) # Actives leaves 12 | return active_leaves 13 | 14 | 15 | def tree_structure_constraints(explainer, solver, active_leaves, instance): 16 | forest = explainer._boosted_trees.forest 17 | for j, tree in enumerate(forest): 18 | for i, leave in enumerate(tree.get_leaves()): 19 | t = TypeLeaf.LEFT if leave.parent.left == leave else TypeLeaf.RIGHT 20 | cube = forest[j].create_cube(leave.parent, t) 21 | nb_neg = sum((1 for l in cube if l < 0)) 22 | nb_pos = sum((1 for l in cube if l > 0)) 23 | constraint = solver.RowConstraint(-solver.infinity(), nb_neg) 24 | constraint.SetCoefficient(active_leaves[j][i], nb_pos + nb_neg) 25 | for lit in cube: 26 | constraint.SetCoefficient(instance[abs(lit) - 1], -1 if lit > 0 else 1) 27 | 28 | # Only one leave activated per tree 29 | for j, tree in enumerate(forest): 30 | constraint = solver.RowConstraint(1, 1) 31 | for v in active_leaves[j]: 32 | constraint.SetCoefficient(v, 1) 33 | 34 | 35 | def theory_constraints(solver, instance, theory): 36 | if theory is not None: 37 | for clause in theory: 38 | constraint = solver.RowConstraint(-solver.infinity(), 0) 39 | for l in clause: 40 | constraint.SetCoefficient(instance[abs(l) - 1], 1 if l < 0 else -1) 41 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/MUS/MUSERSolver.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import time 4 | import uuid 5 | from pyxai.sources.core.tools.utils import get_os 6 | 7 | from pyxai import Explainer 8 | 9 | MUSER_DIRECTORY = os.sep.join(__file__.split(os.sep)[:-1]) + os.sep + "exec" + os.sep 10 | MUSER_EXEC = MUSER_DIRECTORY + "muser-" + get_os() 11 | 12 | class MUSERSolver: 13 | def __init__(self, *, filenames="/tmp/muser-", _hash=str(uuid.uuid4().fields[-1])[:8]): 14 | self._hash = _hash 15 | self.filename_gcnf = filenames + self._hash + ".gcnf" 16 | if get_os() == "darwin": 17 | raise OSError("MUSER is not available on MacOS.") 18 | elif get_os() == "windows": 19 | raise OSError("MUSER is not available on Windows.") 20 | 21 | def write_gcnf(self, n_variables, hard_clauses, soft_clauses): 22 | 23 | file = open(self.filename_gcnf, "w") 24 | file.write(f"p gcnf {n_variables} {len(hard_clauses) + len(soft_clauses)} {len(soft_clauses) + 1}\n") 25 | 26 | for clause in hard_clauses: 27 | file.write("{1} " + " ".join(str(lit) for lit in clause) + " 0\n") 28 | for i, clause in enumerate(soft_clauses): 29 | file.write("{" + str(i + 2) + "} " + " ".join(str(lit) for lit in clause) + " 0\n") 30 | 31 | file.close() 32 | 33 | 34 | def solve(self, time_limit=None): 35 | time_used = -time.time() 36 | try: 37 | p = subprocess.run([MUSER_EXEC, '-comp', '-grp', self.filename_gcnf], timeout=time_limit, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 38 | universal_newlines=True) 39 | time_used += time.time() 40 | except subprocess.TimeoutExpired: 41 | time_used = Explainer.TIMEOUT 42 | model = [line for line in p.stdout.split('\n') if line.startswith("v")][0] 43 | model = [int(lit) for lit in model.split(' ')[1:-1]] 44 | status = [line.split(" ")[1] for line in p.stdout.split("\n") if len(line) > 0 and line[0] == "s"][0] 45 | return model, status, time_used 46 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/MUS/OPTUXSolver.py: -------------------------------------------------------------------------------- 1 | from pysat.examples.optux import OptUx 2 | from pysat.formula import WCNF 3 | 4 | 5 | class OPTUXSolver: 6 | 7 | def __init__(self): 8 | self.WCNF = WCNF() 9 | 10 | 11 | def add_soft_clauses(self, clauses, weight): 12 | for clause in clauses: 13 | self.WCNF.append(clause, weight=weight) 14 | 15 | 16 | def add_hard_clauses(self, clauses): 17 | for clause in clauses: 18 | self.WCNF.append(clause) 19 | 20 | 21 | def solve(self, implicant): 22 | minimal_len = len(implicant) 23 | reason = implicant 24 | with OptUx(self.WCNF) as optux: 25 | for mus in optux.enumerate(): 26 | if len(mus) < minimal_len: 27 | reason = [implicant[index - 1] for index in mus] # enumerate return the indexes of soft clauses 28 | minimal_len = len(mus) 29 | return reason 30 | -------------------------------------------------------------------------------- /pyxai/sources/solvers/MUS/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/sources/solvers/MUS/__init__.py -------------------------------------------------------------------------------- /pyxai/sources/solvers/MUS/exec/muser-linux: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/sources/solvers/MUS/exec/muser-linux -------------------------------------------------------------------------------- /pyxai/sources/solvers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/sources/solvers/__init__.py -------------------------------------------------------------------------------- /pyxai/sources/solvers/old_exec/muser-darwin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/sources/solvers/old_exec/muser-darwin -------------------------------------------------------------------------------- /pyxai/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/tests/__init__.py -------------------------------------------------------------------------------- /pyxai/tests/explaining/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/tests/explaining/__init__.py -------------------------------------------------------------------------------- /pyxai/tests/explaining/bt.py: -------------------------------------------------------------------------------- 1 | from pyxai import Builder, Learning, Explainer, Tools 2 | import unittest 3 | 4 | Tools.set_verbose(0) 5 | 6 | 7 | class TestBT(unittest.TestCase): 8 | model = None 9 | 10 | def init(cls): 11 | if cls.model is None: 12 | cls.learner = Learning.Xgboost("tests/compas.csv", learner_type=Learning.CLASSIFICATION) 13 | cls.model = cls.learner.evaluate(method=Learning.HOLD_OUT, output=Learning.BT) 14 | return cls.learner, cls.model 15 | 16 | 17 | def test_tree_specific(self): 18 | learner, model = self.init() 19 | explainer = Explainer.initialize(model, features_type={"numerical": Learning.DEFAULT}) 20 | instances = learner.get_instances(model, n=30) 21 | for instance, prediction in instances: 22 | explainer.set_instance(instance) 23 | tree_specific_reason = explainer.tree_specific_reason() 24 | self.assertTrue(explainer.is_tree_specific_reason(tree_specific_reason)) 25 | 26 | 27 | def test_excluded(self): 28 | learner, model = self.init() 29 | explainer = Explainer.initialize(model) 30 | explainer.set_excluded_features(['African_American', 'Hispanic']) 31 | instances = learner.get_instances(model, n=30) 32 | 33 | for instance, prediction in instances: 34 | explainer.set_instance(instance) 35 | tree_specific_reason = explainer.tree_specific_reason() 36 | self.assertFalse(explainer.reason_contains_features(tree_specific_reason, 'Hispanic')) 37 | self.assertFalse(explainer.reason_contains_features(tree_specific_reason, 'African_American')) 38 | 39 | explainer.set_excluded_features(['Female']) 40 | for instance, prediction in instances: 41 | explainer.set_instance(instance) 42 | tree_specific_reason = explainer.tree_specific_reason() 43 | self.assertFalse(explainer.reason_contains_features(tree_specific_reason, 'Female')) 44 | 45 | def test_contrastive(self): 46 | learner, model = self.init() 47 | explainer = Explainer.initialize(model) 48 | instances = learner.get_instances(model, n=5) 49 | for instance, prediction in instances: 50 | explainer.set_instance(instance) 51 | contrastive_reason = explainer.minimal_contrastive_reason() 52 | self.assertTrue(len(contrastive_reason) > 0 and explainer.is_contrastive_reason(contrastive_reason)) 53 | 54 | if __name__ == '__main__': 55 | unittest.main(verbosity=2) 56 | -------------------------------------------------------------------------------- /pyxai/tests/explaining/dt.py: -------------------------------------------------------------------------------- 1 | from pyxai import Builder, Learning, Explainer, Tools 2 | import unittest 3 | 4 | Tools.set_verbose(0) 5 | 6 | 7 | class TestDT(unittest.TestCase): 8 | model = None 9 | 10 | 11 | def init(cls): 12 | if cls.model is None: 13 | cls.learner = Learning.Scikitlearn("tests/compas.csv", learner_type=Learning.CLASSIFICATION) 14 | cls.model = cls.learner.evaluate(method=Learning.HOLD_OUT, output=Learning.DT) 15 | return cls.learner, cls.model 16 | 17 | 18 | def test_sufficients(self): 19 | learner, model = self.init() 20 | explainer = Explainer.initialize(model) 21 | instances = learner.get_instances(model, n=30) 22 | for instance, prediction in instances: 23 | explainer.set_instance(instance) 24 | sufficient_reasons = explainer.sufficient_reason(n=10) 25 | for sr in sufficient_reasons: 26 | self.assertTrue(explainer.is_sufficient_reason(sr)) 27 | 28 | 29 | def test_contrastives(self): 30 | learner, model = self.init() 31 | explainer = Explainer.initialize(model) 32 | instances = learner.get_instances(model, n=30) 33 | for instance, prediction in instances: 34 | explainer.set_instance(instance) 35 | contrastives_reasons = explainer.contrastive_reason(n=10) 36 | for sr in contrastives_reasons: 37 | self.assertTrue(explainer.is_contrastive_reason(sr)) 38 | 39 | 40 | def test_minimals(self): 41 | learner, model = self.init() 42 | explainer = Explainer.initialize(model) 43 | instances = learner.get_instances(model, n=30) 44 | for instance, prediction in instances: 45 | explainer.set_instance(instance) 46 | minimal_reasons = explainer.minimal_sufficient_reason(n=10) 47 | for m in minimal_reasons: 48 | self.assertTrue(explainer.is_sufficient_reason(m)) 49 | 50 | 51 | def test_excluded(self): 52 | learner, model = self.init() 53 | explainer = Explainer.initialize(model) 54 | explainer.set_excluded_features(['African_American', 'Hispanic']) 55 | instances = learner.get_instances(model, n=30) 56 | 57 | for instance, prediction in instances: 58 | explainer.set_instance(instance) 59 | sufficient_reasons = explainer.sufficient_reason(n=10) 60 | for sr in sufficient_reasons: 61 | self.assertFalse(explainer.reason_contains_features(sr, 'Hispanic')) 62 | self.assertFalse(explainer.reason_contains_features(sr, 'African_American')) 63 | 64 | contrastives_reasons = explainer.contrastive_reason(n=10) 65 | for sr in contrastives_reasons: 66 | self.assertFalse(explainer.reason_contains_features(sr, 'Hispanic')) 67 | self.assertFalse(explainer.reason_contains_features(sr, 'African_American')) 68 | 69 | explainer.set_excluded_features(['Female']) 70 | for instance, prediction in instances: 71 | explainer.set_instance(instance) 72 | sufficient_reasons = explainer.sufficient_reason(n=10) 73 | for sr in sufficient_reasons: 74 | self.assertFalse(explainer.reason_contains_features(sr, 'Female')) 75 | 76 | contrastives_reasons = explainer.contrastive_reason(n=10) 77 | for sr in contrastives_reasons: 78 | self.assertFalse(explainer.reason_contains_features(sr, 'Female')) 79 | 80 | 81 | if __name__ == '__main__': 82 | unittest.main(verbosity=2) 83 | -------------------------------------------------------------------------------- /pyxai/tests/explaining/misc.py: -------------------------------------------------------------------------------- 1 | from pyxai import Builder, Learning, Explainer, Tools 2 | from pyxai.sources.core.explainer.Explainer import Explainer as exp 3 | import unittest 4 | 5 | Tools.set_verbose(0) 6 | 7 | 8 | class TestMisc(unittest.TestCase): 9 | model = None 10 | 11 | 12 | def init(cls): 13 | if cls.model is None: 14 | cls.learner = Learning.Scikitlearn("tests/iris.csv", learner_type=Learning.CLASSIFICATION) 15 | cls.model = cls.learner.evaluate(method=Learning.HOLD_OUT, output=Learning.DT) 16 | return cls.learner, cls.model 17 | 18 | 19 | def no_instance(self): 20 | learner, model = self.init() 21 | explainer = Explainer.initialize(model) 22 | try: 23 | explainer.direct_reason() 24 | except ValueError: 25 | raise 26 | 27 | 28 | def test_noinstance(self): 29 | with self.assertRaises(ValueError): 30 | self.no_instance() 31 | 32 | 33 | def test_format(self): 34 | self.assertTrue(exp.format([1, 2], n=1) == (1, 2)) 35 | self.assertTrue(exp.format([[1, 2], [3]], n=2) == ((1, 2), (3,))) 36 | self.assertTrue(exp.format([[1]], n=2) == ((1,),)) 37 | 38 | 39 | if __name__ == '__main__': 40 | unittest.main(verbosity=1) 41 | -------------------------------------------------------------------------------- /pyxai/tests/explaining/regressionbt.py: -------------------------------------------------------------------------------- 1 | from pyxai import Builder, Learning, Explainer, Tools 2 | import unittest 3 | 4 | Tools.set_verbose(0) 5 | 6 | 7 | class TestDT(unittest.TestCase): 8 | model = None 9 | 10 | 11 | def init(cls): 12 | if cls.model is None: 13 | cls.learner = Learning.Xgboost("tests/winequality-red.csv", learner_type=Learning.REGRESSION) 14 | cls.model = cls.learner.evaluate(method=Learning.HOLD_OUT, output=Learning.BT) 15 | return cls.learner, cls.model 16 | 17 | @unittest.skip("Bad display ...") 18 | def test_tree_specific(self): 19 | learner, model = self.init() 20 | explainer = Explainer.initialize(model, features_type={"numerical": Learning.DEFAULT}) 21 | instances = learner.get_instances(model, n=30) 22 | for instance, prediction in instances: 23 | explainer.set_instance(instance) 24 | tree_specific_reason = explainer.tree_specific_reason() 25 | self.assertTrue(explainer.is_tree_specific_reason(tree_specific_reason)) 26 | 27 | @unittest.skip("Bad display ...") 28 | def test_sufficient(self): 29 | learner, model = self.init() 30 | explainer = Explainer.initialize(model, features_type={"numerical": Learning.DEFAULT}) 31 | instances = learner.get_instances(model, n=30) 32 | for instance, prediction in instances: 33 | explainer.set_instance(instance) 34 | sufficient_reason = explainer.sufficient_reason(time_limit=5) 35 | self.assertTrue(explainer.is_reason(sufficient_reason)) 36 | 37 | 38 | if __name__ == '__main__': 39 | unittest.main(verbosity=2) 40 | -------------------------------------------------------------------------------- /pyxai/tests/explaining/rf-multiclasses.py: -------------------------------------------------------------------------------- 1 | from pyxai import Builder, Learning, Explainer, Tools 2 | import unittest 3 | 4 | Tools.set_verbose(0) 5 | 6 | 7 | class TestRFMulticlasses(unittest.TestCase): 8 | model = None 9 | 10 | 11 | def init(cls): 12 | if cls.model is None: 13 | cls.learner = Learning.Scikitlearn("tests/iris.csv", learner_type=Learning.CLASSIFICATION) 14 | cls.model = cls.learner.evaluate(method=Learning.HOLD_OUT, output=Learning.RF) 15 | return cls.learner, cls.model 16 | 17 | 18 | def test_direct(self): 19 | learner, model = self.init() 20 | explainer = Explainer.initialize(model, features_type={"numerical": Learning.DEFAULT}) 21 | instances = learner.get_instances(model, n=30) 22 | for instance, prediction in instances: 23 | explainer.set_instance(instance) 24 | direct_reason = explainer.direct_reason() 25 | 26 | def test_sufficient(self): 27 | learner, model = self.init() 28 | explainer = Explainer.initialize(model, features_type={"numerical": Learning.DEFAULT}) 29 | instances = learner.get_instances(model, n=30) 30 | for instance, prediction in instances: 31 | explainer.set_instance(instance) 32 | sufficient_reason = explainer.sufficient_reason(time_limit=5) 33 | if explainer.elapsed_time == Explainer.TIMEOUT: 34 | self.assertTrue(explainer.is_reason(sufficient_reason)) 35 | else: 36 | self.assertTrue(explainer.is_sufficient_reason(sufficient_reason) != False) 37 | 38 | 39 | def test_majoritary(self): 40 | learner, model = self.init() 41 | explainer = Explainer.initialize(model, features_type={"numerical": Learning.DEFAULT}) 42 | instances = learner.get_instances(model, n=30) 43 | for instance, prediction in instances: 44 | explainer.set_instance(instance) 45 | majoritary_reason = explainer.majoritary_reason() 46 | self.assertTrue(explainer.is_majoritary_reason(majoritary_reason)) 47 | 48 | 49 | def test_minimal_majoritary(self): 50 | learner, model = self.init() 51 | explainer = Explainer.initialize(model) # ), features_type={"numerical": Learning.DEFAULT}) 52 | instances = learner.get_instances(model, n=10) 53 | for instance, prediction in instances: 54 | explainer.set_instance(instance) 55 | majoritary_reason = explainer.minimal_majoritary_reason(time_limit=5) 56 | self.assertTrue(len(majoritary_reason) == 0 or explainer.is_majoritary_reason(majoritary_reason)) 57 | 58 | 59 | def test_contrastive(self): 60 | learner, model = self.init() 61 | explainer = Explainer.initialize(model) 62 | instances = learner.get_instances(model, n=10) 63 | for instance, prediction in instances: 64 | explainer.set_instance(instance) 65 | contrastive_reason = explainer.minimal_contrastive_reason(time_limit=5) 66 | self.assertTrue(len(contrastive_reason) == 0 or explainer.is_contrastive_reason(contrastive_reason)) 67 | 68 | 69 | if __name__ == '__main__': 70 | unittest.main(verbosity=2) 71 | -------------------------------------------------------------------------------- /pyxai/tests/functionality/ToFeatures.py: -------------------------------------------------------------------------------- 1 | from pyxai import Builder, Learning, Explainer, Tools 2 | import math 3 | 4 | 5 | Tools.set_verbose(0) 6 | 7 | import unittest 8 | class TestToFeatures(unittest.TestCase): 9 | 10 | def test_to_features(self): 11 | node_t1_v1_1 = Builder.DecisionNode(1, operator=Builder.GE, threshold=0, left=0, right=0) 12 | node_t1_v1_1_1 = Builder.DecisionNode(1, operator=Builder.GE, threshold=10, left=node_t1_v1_1, right=0) 13 | node_t1_v1_2 = Builder.DecisionNode(1, operator=Builder.GE, threshold=20, left=node_t1_v1_1_1, right=0) 14 | node_t1_v1_3 = Builder.DecisionNode(1, operator=Builder.GT, threshold=30, left=node_t1_v1_2, right=1) 15 | node_t1_v1_4 = Builder.DecisionNode(1, operator=Builder.GE, threshold=40, left=node_t1_v1_3, right=1) 16 | tree1 = Builder.DecisionTree(1, node_t1_v1_4) 17 | 18 | tree2 = Builder.DecisionTree(1, Builder.LeafNode(1)) 19 | 20 | forest = Builder.RandomForest([tree1, tree2], n_classes=2) 21 | 22 | alice = (18,) 23 | #print("alice:", alice) 24 | explainer = Explainer.initialize(forest, instance=alice) 25 | #print("binary representation: ", explainer.binary_representation) 26 | #print("binary representation features:", explainer.to_features(explainer.binary_representation, eliminate_redundant_features=False)) 27 | #print("target_prediction:", explainer.target_prediction) 28 | 29 | explainer = Explainer.initialize(forest, instance=alice, features_type={"numerical": ["f1"]}) 30 | 31 | test_1 = explainer.binary_representation 32 | #print("test_1:", test_1) 33 | #print("test_1:", explainer.to_features(test_1)) 34 | self.assertEqual(explainer.to_features(test_1),('f1 in [10, 20[',)) 35 | 36 | test_2 = explainer.minimal_contrastive_reason(n=1) 37 | 38 | #print("test_2:", test_2) 39 | #print("test_2:", explainer.to_features(test_2, contrastive=True)) 40 | self.assertEqual(explainer.to_features(test_2, contrastive=True),('f1 <= 30',)) 41 | 42 | test_3 = explainer.minimal_sufficient_reason() 43 | #print("test_3:", test_3) 44 | #print("test_3:", explainer.to_features(test_3)) 45 | self.assertEqual(explainer.to_features(test_3),('f1 < 20',)) 46 | 47 | if __name__ == '__main__': 48 | print("Tests: " + TestToFeatures.__name__ + ":") 49 | unittest.main() -------------------------------------------------------------------------------- /pyxai/tests/functionality/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/tests/functionality/__init__.py -------------------------------------------------------------------------------- /pyxai/tests/importing/SimpleScikitLearn.py: -------------------------------------------------------------------------------- 1 | from pyxai import Tools, Learning, Explainer 2 | from sklearn.ensemble import RandomForestClassifier 3 | from sklearn.tree import DecisionTreeClassifier 4 | from sklearn.model_selection import LeaveOneGroupOut 5 | from sklearn import svm 6 | from sklearn import datasets 7 | from sklearn.ensemble import RandomForestClassifier 8 | 9 | import pandas 10 | import numpy 11 | import random 12 | import functools 13 | import operator 14 | import copy 15 | 16 | Tools.set_verbose(0) 17 | 18 | import unittest 19 | 20 | 21 | class TestImportSimpleScikitlearn(unittest.TestCase): 22 | PRECISION = 3 23 | 24 | 25 | def test_simple_RF_breast_cancer(self): 26 | model_rf = RandomForestClassifier(random_state=0) 27 | data = datasets.load_breast_cancer(as_frame=True) 28 | X = data.data.to_numpy() 29 | Y = data.target.to_numpy() 30 | 31 | feature_names = data.feature_names 32 | model_rf.fit(X, Y) 33 | 34 | learner, model = Learning.import_models(model_rf, feature_names) 35 | instance, prediction = learner.get_instances(dataset=data.frame, model=model, n=1) 36 | 37 | explainer = Explainer.initialize(model, instance=instance) 38 | 39 | direct = explainer.direct_reason() 40 | 41 | try: 42 | sufficient = explainer.sufficient_reason() 43 | except OSError: 44 | pass 45 | 46 | def test_simple_RF_iris(self): 47 | model_rf = RandomForestClassifier(random_state=0) 48 | data = datasets.load_iris(as_frame=True) 49 | X = data.data.to_numpy() 50 | Y = data.target.to_numpy() 51 | 52 | feature_names = data.feature_names 53 | model_rf.fit(X, Y) 54 | 55 | learner, model = Learning.import_models(model_rf) 56 | instance, prediction = learner.get_instances(dataset=data.frame, model=model, n=1) 57 | 58 | explainer = Explainer.initialize(model, instance=instance) 59 | 60 | direct = explainer.direct_reason() 61 | 62 | try: 63 | sufficient = explainer.sufficient_reason() 64 | except OSError: 65 | pass 66 | 67 | 68 | if __name__ == '__main__': 69 | print("Tests: " + TestImportSimpleScikitlearn.__name__ + ":") 70 | unittest.main() 71 | -------------------------------------------------------------------------------- /pyxai/tests/importing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/tests/importing/__init__.py -------------------------------------------------------------------------------- /pyxai/tests/learning/LightGBM.py: -------------------------------------------------------------------------------- 1 | from pyxai import Learning, Explainer, Tools 2 | import math 3 | 4 | Tools.set_verbose(0) 5 | 6 | import unittest 7 | 8 | 9 | class TestLearningLightGBM(unittest.TestCase): 10 | 11 | def test_parameters(self): 12 | learner = Learning.LightGBM("tests/dermatology.csv", learner_type=Learning.REGRESSION) 13 | models = learner.evaluate(method=Learning.K_FOLDS, output=Learning.BT, test_size=0.2, learning_rate=0.3, n_estimators=5) 14 | for model in models: 15 | self.assertEqual(model.raw_model.booster_.num_trees(), 5) # for n_estimators 16 | self.assertEqual(model.raw_model.get_params()["learning_rate"], 0.3) 17 | 18 | 19 | 20 | def test_regression_dermatology(self): 21 | self.regression(Learning.LightGBM("tests/dermatology.csv", learner_type=Learning.REGRESSION)) 22 | 23 | 24 | def test_regression_wine(self): 25 | self.regression(Learning.LightGBM("tests/winequality-red.csv", learner_type=Learning.REGRESSION)) 26 | 27 | 28 | def regression(self, learner): 29 | models = learner.evaluate(method=Learning.K_FOLDS, output=Learning.BT, test_size=0.2) 30 | for model in models: 31 | instances = learner.get_instances(model=model, n=10, indexes=Learning.TEST) 32 | for i, (instance, prediction_classifier) in enumerate(instances): 33 | prediction_classifier = float(prediction_classifier) 34 | prediction_model_1 = model.predict_instance(instance) 35 | implicant = model.instance_to_binaries(instance) 36 | prediction_model_2 = model.predict_implicant(implicant) 37 | self.assertEqual(prediction_classifier, prediction_model_1) 38 | self.assertEqual(prediction_classifier, prediction_model_2) 39 | 40 | 41 | if __name__ == '__main__': 42 | print("Tests: " + TestLearningLightGBM.__name__ + ":") 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /pyxai/tests/learning/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/tests/learning/__init__.py -------------------------------------------------------------------------------- /pyxai/tests/saveload/LightGBM.py: -------------------------------------------------------------------------------- 1 | from pyxai import Learning, Explainer, Tools 2 | import math 3 | import shutil 4 | from decimal import Decimal 5 | 6 | Tools.set_verbose(0) 7 | from collections.abc import Iterable 8 | import unittest 9 | 10 | 11 | class TestSaveLoadLightGBM(unittest.TestCase): 12 | 13 | def test_hold_out_regression(self): 14 | self.launch_save("tests/winequality-red.csv", Learning.HOLD_OUT, Learning.BT, Learning.REGRESSION) 15 | self.launch_load("tests/winequality-red.csv", Learning.REGRESSION) 16 | shutil.rmtree("try_save") 17 | 18 | 19 | def test_k_folds_regression(self): 20 | self.launch_save("tests/winequality-red.csv", Learning.K_FOLDS, Learning.BT, Learning.REGRESSION) 21 | self.launch_load("tests/winequality-red.csv", Learning.REGRESSION) 22 | shutil.rmtree("try_save") 23 | 24 | 25 | def test_leave_one_group_out_regression(self): 26 | self.launch_save("tests/winequality-red.csv", Learning.LEAVE_ONE_GROUP_OUT, Learning.BT, Learning.REGRESSION) 27 | self.launch_load("tests/winequality-red.csv", Learning.REGRESSION) 28 | shutil.rmtree("try_save") 29 | 30 | def launch_save(self, dataset, method, output, learner_type): 31 | learner = Learning.LightGBM(dataset, learner_type=learner_type) 32 | models = learner.evaluate(method=method, output=output, test_size=0.2) 33 | learner.save(models, "try_save") 34 | 35 | 36 | def launch_load(self, dataset, learner_type): 37 | learner, models = Learning.load(models_directory="try_save", tests=True, dataset=dataset) 38 | if not isinstance(models, Iterable): 39 | models = [models] 40 | for model in models: 41 | instances = learner.get_instances(model=model, n=10, indexes=Learning.TEST) 42 | for (instance, prediction_classifier) in instances: 43 | prediction_model_1 = model.predict_instance(instance) 44 | implicant = model.instance_to_binaries(instance) 45 | prediction_model_2 = model.predict_implicant(implicant) 46 | self.assertEqual(prediction_classifier, prediction_model_1) 47 | self.assertEqual(prediction_classifier, prediction_model_2) 48 | 49 | 50 | if __name__ == '__main__': 51 | print("Tests: " + TestSaveLoadLightGBM.__name__ + ":") 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /pyxai/tests/saveload/ScikitLearn.py: -------------------------------------------------------------------------------- 1 | from pyxai import Learning, Explainer, Tools 2 | import math 3 | from decimal import Decimal 4 | from collections.abc import Iterable 5 | import shutil 6 | Tools.set_verbose(0) 7 | 8 | import unittest 9 | 10 | 11 | class TestSaveLoadScikitlearn(unittest.TestCase): 12 | PRECISION = 3 13 | 14 | 15 | def test_hold_out_DT(self): 16 | self.launch_save("tests/dermatology.csv", Learning.HOLD_OUT, Learning.DT) 17 | self.launch_load("tests/dermatology.csv") 18 | self.launch_save("tests/iris.csv", Learning.HOLD_OUT, Learning.DT) 19 | self.launch_load("tests/iris.csv") 20 | shutil.rmtree("try_save") 21 | 22 | def test_k_folds_DT(self): 23 | self.launch_save("tests/dermatology.csv", Learning.K_FOLDS, Learning.DT) 24 | self.launch_load("tests/dermatology.csv") 25 | self.launch_save("tests/iris.csv", Learning.K_FOLDS, Learning.DT) 26 | self.launch_load("tests/iris.csv") 27 | shutil.rmtree("try_save") 28 | 29 | def test_leave_one_group_out_DT(self): 30 | self.launch_save("tests/dermatology.csv", Learning.LEAVE_ONE_GROUP_OUT, Learning.DT) 31 | self.launch_load("tests/dermatology.csv") 32 | self.launch_save("tests/iris.csv", Learning.LEAVE_ONE_GROUP_OUT, Learning.DT) 33 | self.launch_load("tests/iris.csv") 34 | shutil.rmtree("try_save") 35 | 36 | def test_hold_out_RF(self): 37 | self.launch_save("tests/dermatology.csv", Learning.HOLD_OUT, Learning.RF) 38 | self.launch_load("tests/dermatology.csv") 39 | self.launch_save("tests/iris.csv", Learning.HOLD_OUT, Learning.RF) 40 | self.launch_load("tests/iris.csv") 41 | shutil.rmtree("try_save") 42 | 43 | def test_k_folds_RF(self): 44 | self.launch_save("tests/dermatology.csv", Learning.K_FOLDS, Learning.RF) 45 | self.launch_load("tests/dermatology.csv") 46 | self.launch_save("tests/iris.csv", Learning.K_FOLDS, Learning.RF) 47 | self.launch_load("tests/iris.csv") 48 | shutil.rmtree("try_save") 49 | 50 | def test_leave_one_group_out_RF(self): 51 | self.launch_save("tests/dermatology.csv", Learning.LEAVE_ONE_GROUP_OUT, Learning.RF) 52 | self.launch_load("tests/dermatology.csv") 53 | self.launch_save("tests/iris.csv", Learning.LEAVE_ONE_GROUP_OUT, Learning.RF) 54 | self.launch_load("tests/iris.csv") 55 | shutil.rmtree("try_save") 56 | 57 | def launch_save(self, dataset, method, output): 58 | learner = Learning.Scikitlearn(dataset, learner_type=Learning.CLASSIFICATION) 59 | models = learner.evaluate(method=method, output=output, test_size=0.2) 60 | learner.save(models, "try_save") 61 | 62 | 63 | def launch_load(self, dataset): 64 | learner, models = Learning.load(models_directory="try_save", tests=True, dataset=dataset) 65 | if not isinstance(models, Iterable): 66 | models = [models] 67 | for model in models: 68 | instances = learner.get_instances(model=model, n=10, indexes=Learning.TEST) 69 | for (instance, prediction_classifier) in instances: 70 | prediction_model_1 = model.predict_instance(instance) 71 | implicant = model.instance_to_binaries(instance) 72 | prediction_model_2 = model.predict_implicant(implicant) 73 | self.assertEqual(prediction_classifier, prediction_model_1) 74 | self.assertEqual(prediction_classifier, prediction_model_2) 75 | 76 | 77 | if __name__ == '__main__': 78 | print("Tests: " + TestSaveLoadScikitlearn.__name__ + ":") 79 | unittest.main() 80 | -------------------------------------------------------------------------------- /pyxai/tests/saveload/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crillab/pyxai/6fe3c85b24d13bab71be0a6b5ec478a326495b67/pyxai/tests/saveload/__init__.py -------------------------------------------------------------------------------- /pyxai/tests/tests.py: -------------------------------------------------------------------------------- 1 | # import os 2 | import unittest 3 | import logging 4 | import sys 5 | import platform 6 | 7 | from pyxai.tests.functionality.GetInstances import * 8 | from pyxai.tests.functionality.ToFeatures import * 9 | from pyxai.tests.functionality.Metrics import * 10 | from pyxai.tests.functionality.Rectify import * 11 | from pyxai.tests.learning.ScikitLearn import * 12 | from pyxai.tests.learning.LightGBM import * 13 | from pyxai.tests.learning.XGBoost import * 14 | from pyxai.tests.saveload.XGBoost import * 15 | from pyxai.tests.saveload.ScikitLearn import * 16 | from pyxai.tests.saveload.LightGBM import * 17 | from pyxai.tests.importing.ScikitLearn import * 18 | from pyxai.tests.importing.SimpleScikitLearn import * 19 | from pyxai.tests.importing.LightGBM import * 20 | from pyxai.tests.importing.XGBoost import * 21 | from pyxai.tests.explaining.dt import * 22 | from pyxai.tests.explaining.misc import * 23 | from pyxai.tests.explaining.rf import * 24 | from pyxai.tests.explaining.bt import * 25 | from pyxai.tests.explaining.regressionbt import * 26 | 27 | def linux_tests(): 28 | suite = unittest.TestSuite() 29 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestToFeatures)) 30 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestGetInstances)) 31 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestMetrics)) 32 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestRectify)) 33 | 34 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestLearningScikitlearn)) 35 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestLearningXGBoost)) 36 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestLearningLightGBM)) 37 | 38 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestSaveLoadScikitlearn)) 39 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestSaveLoadXgboost)) 40 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestSaveLoadLightGBM)) 41 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestImportScikitlearn)) 42 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestImportSimpleScikitlearn)) 43 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestImportXGBoost)) 44 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestImportLightGBM)) 45 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestMisc)) 46 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestDT)) 47 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestRF)) 48 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestBT)) 49 | return suite 50 | 51 | def windows_tests(): 52 | suite = unittest.TestSuite() 53 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestToFeatures)) 54 | return suite 55 | 56 | 57 | if __name__ == '__main__': 58 | logging.basicConfig(stream=sys.stdout, level=logging.INFO) 59 | runner = unittest.TextTestRunner(verbosity=2) 60 | if platform.system() == 'Windows': 61 | runner.run(windows_tests()) 62 | else: 63 | runner.run(linux_tests()) 64 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, Extension 3 | 4 | __cxx_path__ = "pyxai/sources/solvers/GREEDY/src/" 5 | __cxx_files__ = [__cxx_path__+f for f in os.listdir(__cxx_path__) if f.endswith(".cc")]+[__cxx_path__+"bcp/"+f for f in os.listdir(__cxx_path__+"bcp/") if f.endswith(".cc")] 6 | __cxx_headers__ = [__cxx_path__+f for f in os.listdir(__cxx_path__) if f.endswith(".h")]+[__cxx_path__+"bcp/"+f for f in os.listdir(__cxx_path__+"bcp/") if f.endswith(".h")] 7 | 8 | print("__cxx_path__:", __cxx_path__) 9 | print("__cxx_files__:", __cxx_files__) 10 | print("__cxx_headers__:", __cxx_headers__) 11 | 12 | setup(ext_modules=[Extension( 13 | "c_explainer", 14 | __cxx_files__, 15 | language="c++", 16 | extra_compile_args=["-std=c++11"] 17 | )], 18 | headers=__cxx_headers__, 19 | ) 20 | --------------------------------------------------------------------------------