sklearn-morpho 0.2.2__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/.SRCINFO +3 -3
  2. sklearn_morpho-0.2.3/.gitattributes +1 -0
  3. sklearn_morpho-0.2.3/.github/workflows/code_quality.yml +47 -0
  4. sklearn_morpho-0.2.2/.github/workflows/pypi-publish.yml → sklearn_morpho-0.2.3/.github/workflows/pypi_publish.yml +2 -1
  5. sklearn_morpho-0.2.3/.github/workflows/tests.yml +68 -0
  6. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/.gitignore +1 -1
  7. sklearn_morpho-0.2.3/PKG-INFO +30 -0
  8. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/PKGBUILD +3 -3
  9. sklearn_morpho-0.2.3/README-PyPI.md +14 -0
  10. sklearn_morpho-0.2.3/README.md +101 -0
  11. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/pyproject.toml +36 -2
  12. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/testing/compare_estimators.py +13 -14
  13. sklearn_morpho-0.2.3/testing/compare_estimators_multiprocess.py +368 -0
  14. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/testing/compare_estimators_show.py +24 -13
  15. sklearn_morpho-0.2.2/.github/workflows/ci.yml +0 -90
  16. sklearn_morpho-0.2.2/PKG-INFO +0 -10
  17. sklearn_morpho-0.2.2/README.md +0 -56
  18. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/.gitlab-ci.yml +0 -0
  19. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/.qbuild/scripts/coverage +0 -0
  20. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/.qbuild/scripts/main +0 -0
  21. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/.qbuild/scripts/tests +0 -0
  22. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/LICENSE +0 -0
  23. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/MREs/l-DEP.ipynb +0 -0
  24. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/MREs/r-DEP.ipynb +0 -0
  25. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/MREs/simple_perceptron.ipynb +0 -0
  26. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/MREs/train_multiclass.ipynb +0 -0
  27. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/check_todos.sh +0 -0
  28. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/pkgbuild_helper.sh +0 -0
  29. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/pyrightconfig.json +0 -0
  30. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/__init__.py +0 -0
  31. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/classifiers/__init__.py +0 -0
  32. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/classifiers/ldep.py +0 -0
  33. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/classifiers/rdep.py +0 -0
  34. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/classifiers/simple_perceptron.py +0 -0
  35. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/stopping/__init__.py +0 -0
  36. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/stopping/stopping_base.py +0 -0
  37. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/stopping/stopping_cost.py +0 -0
  38. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/stopping/stopping_early.py +0 -0
  39. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/stopping/stopping_epoch.py +0 -0
  40. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/stopping/stopping_train_stop.py +0 -0
  41. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/training/__init__.py +0 -0
  42. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/training/dccp_ldep.py +0 -0
  43. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/training/dccp_rdep.py +0 -0
  44. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/training/dccp_simple_perceptron.py +0 -0
  45. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/training/dccp_wrapper.py +0 -0
  46. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/weighting/__init__.py +0 -0
  47. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/weighting/weighting_base.py +0 -0
  48. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/weighting/weighting_dist.py +0 -0
  49. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/src/sklearn_morpho/weighting/weighting_none.py +0 -0
  50. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/testing/display_boundary.py +0 -0
  51. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/tests/integration/sklearn_test.py +0 -0
  52. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/tests/stopping/cost_test.py +0 -0
  53. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/tests/stopping/early_test.py +0 -0
  54. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/tests/stopping/epoch_test.py +0 -0
  55. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/tests/stopping/train_stop_test.py +0 -0
  56. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/tests/training/friendly_dataset.py +0 -0
  57. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/tests/training/test_dccp_wrapper.py +0 -0
  58. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/tests/training/test_ldep.py +0 -0
  59. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/tests/training/test_rdep.py +0 -0
  60. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/tests/training/test_simple_perceptron.py +0 -0
  61. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/tests/weighting/dist_test.py +0 -0
  62. {sklearn_morpho-0.2.2 → sklearn_morpho-0.2.3}/tests/weighting/none_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  pkgbase = python-sklearn-morpho
2
2
  pkgdesc = Scikit-learn estimator toolbox for morphological perceptrons
3
- pkgver = 0.2.1
3
+ pkgver = 0.2.2
4
4
  pkgrel = 1
5
5
  url = https://github.com/d-002/sklearn-morpho
6
6
  arch = any
@@ -13,7 +13,7 @@ pkgbase = python-sklearn-morpho
13
13
  depends = python-cvxpy
14
14
  depends = python-pytest
15
15
  depends = python-scikit-learn
16
- source = https://files.pythonhosted.org/packages/source/s/sklearn_morpho/sklearn_morpho-0.2.1.tar.gz
17
- sha256sums = aaf94e85e2f546928ace464bf776c08681cd5349a48012076c3c66a9e801961f
16
+ source = https://files.pythonhosted.org/packages/source/s/sklearn_morpho/sklearn_morpho-0.2.2.tar.gz
17
+ sha256sums = 4dc5a155a32332c4da33f375c7e4e83af15fa2a0ea8bb68f1843e74d0b47bce9
18
18
 
19
19
  pkgname = python-sklearn-morpho
@@ -0,0 +1 @@
1
+ *.ipynb linguist-vendored
@@ -0,0 +1,47 @@
1
+ name: Code quality
2
+
3
+ on:
4
+ push:
5
+ branches: [ "master" ]
6
+ pull_request:
7
+ branches: [ "master" ]
8
+
9
+ jobs:
10
+ todo:
11
+ name: Check for TODOs in code
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - name: Checkout repository
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Scan for TODOs in the code
19
+ run: ./check_todos.sh
20
+
21
+ lint:
22
+ name: Check for ${{ matrix.name }} errors
23
+ runs-on: ubuntu-latest
24
+ strategy:
25
+ fail-fast: false
26
+ matrix:
27
+ include:
28
+ - name: "linting"
29
+ command: "python -m ruff check"
30
+ - name: "coding style"
31
+ command: "[ -z \"$(python -m ruff format | grep reformatted)\" ]"
32
+
33
+ steps:
34
+ - name: Checkout repository
35
+ uses: actions/checkout@v4
36
+
37
+ - name: Set up Python
38
+ uses: actions/setup-python@v5
39
+ with:
40
+ python-version: "3.13"
41
+ cache: "pip"
42
+
43
+ - name: Install linter
44
+ run: pip install ruff
45
+
46
+ - name: Run linter for coding style errors
47
+ run: ${{ matrix.command }}
@@ -20,7 +20,8 @@ jobs:
20
20
  - name: Set up Python
21
21
  uses: actions/setup-python@v5
22
22
  with:
23
- python-version: '3.14'
23
+ python-version: "3.13"
24
+ cache: "pip"
24
25
 
25
26
  - name: Install Hatch
26
27
  run: pip install -U pip hatch
@@ -0,0 +1,68 @@
1
+ name: Testsuite
2
+
3
+ on:
4
+ push:
5
+ branches: [ "master" ]
6
+ pull_request:
7
+ branches: [ "master" ]
8
+
9
+ jobs:
10
+ test:
11
+ name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ os: [ "ubuntu-latest" ]
17
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
18
+ # also test on other OSes, but not for all versions to save minutes
19
+ include:
20
+ - os: windows-latest
21
+ python-version: "3.13"
22
+ - os: macos-latest
23
+ python-version: "3.13"
24
+
25
+ steps:
26
+ - name: Checkout repository
27
+ uses: actions/checkout@v4
28
+
29
+ - name: Set up Python ${{ matrix.python-version }}
30
+ uses: actions/setup-python@v5
31
+ with:
32
+ python-version: ${{ matrix.python-version }}
33
+ cache: "pip"
34
+
35
+ - name: Install Hatch
36
+ run: pip install hatch
37
+
38
+ - name: Run tests with Hatch
39
+ run: |
40
+ if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then
41
+ hatch run test.py${{ matrix.python-version }}:pytest
42
+ else
43
+ hatch run pytest
44
+ fi
45
+
46
+ coverage:
47
+ name: Coverage test (ignore failed tests)
48
+ runs-on: ubuntu-latest
49
+
50
+ steps:
51
+ - name: Checkout repository
52
+ uses: actions/checkout@v4
53
+
54
+ - name: Set up Python
55
+ uses: actions/setup-python@v5
56
+ with:
57
+ python-version: "3.13"
58
+ cache: "pip"
59
+
60
+ - name: Install Hatch
61
+ run: pip install hatch
62
+
63
+ - name: Install coverage utilities
64
+ run: pip install coverage
65
+
66
+ - name: Check coverage
67
+ run: |
68
+ hatch run pytest --cov --cov-fail-under=90
@@ -20,7 +20,7 @@ dccp.log
20
20
  WDBC.dat.txt
21
21
 
22
22
  # comparison
23
- comparison_data.json
23
+ comparison.json
24
24
 
25
25
  # jupyter
26
26
  .ipynb_checkpoints
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: sklearn-morpho
3
+ Version: 0.2.3
4
+ License-File: LICENSE
5
+ Classifier: Programming Language :: Python :: 3.11
6
+ Classifier: Programming Language :: Python :: 3.12
7
+ Classifier: Programming Language :: Python :: 3.13
8
+ Classifier: Programming Language :: Python :: 3.14
9
+ Requires-Python: >=3.11
10
+ Requires-Dist: cvxpy<2.0.0,>=1.9.0
11
+ Requires-Dist: numpy>=2.0.0
12
+ Requires-Dist: scikit-learn<2.0.0,>=1.8.0
13
+ Provides-Extra: dccp
14
+ Requires-Dist: dccp<2.0.0,>=1.1.1; extra == 'dccp'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # sklearn-morpho
18
+
19
+ Scikit-learn classifiers toolbox for morphological perceptrons.
20
+
21
+ ## Current features
22
+
23
+ - Scikit-learn classifiers:
24
+ - Linear Dilation-Erosion Perceptron (l-DEP)
25
+ - Reduced Dilation-Erosion Perceptron (r-DEP)
26
+ - Simple Dilation and Erosion Morphological Perceptrons
27
+ - Modular wrapper for DCCP optimization tasks with `cvxpy`
28
+
29
+ More information and explanations
30
+ [here](https://github.com/d-002/sklearn-morpho).
@@ -1,15 +1,15 @@
1
1
  pkgname=python-sklearn-morpho
2
2
  _name=sklearn_morpho
3
- pkgver=0.2.2
3
+ pkgver=0.2.3
4
4
  pkgrel=1
5
5
  pkgdesc="Scikit-learn estimator toolbox for morphological perceptrons"
6
6
  arch=("any")
7
7
  url="https://github.com/d-002/sklearn-morpho"
8
8
  license=("MIT")
9
- depends=("python" "python-cvxpy" "python-pytest" "python-scikit-learn")
9
+ depends=("python" "python-cvxpy" "python-numpy" "python-scikit-learn")
10
10
  makedepends=("python-build" "python-installer" "python-wheel" "python-hatchling")
11
11
  source=("https://files.pythonhosted.org/packages/source/${_name::1}/${_name}/${_name}-${pkgver}.tar.gz")
12
- sha256sums=('aaf94e85e2f546928ace464bf776c08681cd5349a48012076c3c66a9e801961f')
12
+ sha256sums=('4dc5a155a32332c4da33f375c7e4e83af15fa2a0ea8bb68f1843e74d0b47bce9')
13
13
 
14
14
  build() {
15
15
  cd "${_name}-${pkgver}"
@@ -0,0 +1,14 @@
1
+ # sklearn-morpho
2
+
3
+ Scikit-learn classifiers toolbox for morphological perceptrons.
4
+
5
+ ## Current features
6
+
7
+ - Scikit-learn classifiers:
8
+ - Linear Dilation-Erosion Perceptron (l-DEP)
9
+ - Reduced Dilation-Erosion Perceptron (r-DEP)
10
+ - Simple Dilation and Erosion Morphological Perceptrons
11
+ - Modular wrapper for DCCP optimization tasks with `cvxpy`
12
+
13
+ More information and explanations
14
+ [here](https://github.com/d-002/sklearn-morpho).
@@ -0,0 +1,101 @@
1
+ # sklearn-morpho
2
+
3
+ Scikit-learn classifiers toolbox for [morphological perceptrons](#what-are-morphological-perceptrons).
4
+
5
+ ![PyPI package](https://img.shields.io/pypi/v/sklearn-morpho?style=flat-square)
6
+ ![Supported Python version](https://img.shields.io/pypi/pyversions/sklearn-morpho?style=flat-square)
7
+ ![AUR package](https://img.shields.io/aur/version/python-sklearn-morpho?style=flat-square)
8
+ ![Code quality](https://img.shields.io/github/actions/workflow/status/d-002/sklearn-morpho/code_quality.yml?style=flat-square&label=Code%20quality)
9
+ ![Tests](https://img.shields.io/github/actions/workflow/status/d-002/sklearn-morpho/tests.yml?style=flat-square&label=Tests)
10
+
11
+ ## Current features
12
+
13
+ - Scikit-learn classifiers:
14
+ - Linear Dilation-Erosion Perceptron (l-DEP)
15
+ - Reduced Dilation-Erosion Perceptron (r-DEP)
16
+ - Simple Dilation and Erosion Morphological Perceptrons
17
+ - Modular wrapper for DCCP optimization tasks with `cvxpy`
18
+
19
+ File tree:
20
+
21
+ - `src/sklearn_morpho`: contains the source code and a testsuite in its `tests`
22
+ subdirectory.
23
+ - `tests`: pytest testsuite.
24
+ - `testing`: standalone files that use this library.
25
+ They are used internally for testing, but they are not tests nor part of the
26
+ library.
27
+ - `MREs`: standalone jupyter notebooks to showcase some of this library's
28
+ features.
29
+
30
+ ## Getting started
31
+
32
+ Take a look at the Jupyter code examples in the
33
+ [MREs](https://github.com/d-002/sklearn-morpho/tree/master/MREs) directory.
34
+
35
+ ## Running the project
36
+
37
+ Install Python 3 and hatch.
38
+ Then run one of these commands:
39
+
40
+ - `hatch run jupyter lab` to run the Jupyter notebooks
41
+ - `hatch run pytest` for tests
42
+ - `hatch shell` to run testing files like `testing/display_boundary.py` in the
43
+ right environment.
44
+
45
+ Special note for the estimators comparison testing files: they are split in two
46
+ files to avoid training the estimators every time one wants to view the results.
47
+
48
+ ## Contributing
49
+
50
+ As the project is open source, any help is greatly appreciated!
51
+
52
+ To keep code clean, this repository uses a CI/CD pipeline with tests and an
53
+ enforced coding style, which can all be found in the project's configuration
54
+ files.
55
+
56
+ Please adhere to these rules when contributing.
57
+
58
+ Regarding AI pull requests, they are generally discouraged as you should be able
59
+ to understand and help maintaining any features you add to this repo.
60
+
61
+ ## For Arch users
62
+
63
+ The Python package is available in the
64
+ [Arch User Repository](https://aur.archlinux.org) as `python-sklearn-morpho`, as
65
+ are all its dependencies not already in the official packages, except for
66
+ `dccp`.
67
+
68
+ Since the latter is an optional dependency, you can either:
69
+
70
+ - Download `dccp` through pip or use `pip install sklearn-morpho[dccp]` in a
71
+ Python virtual environment.
72
+ - Download `python-sklearn-morpho` from the AUR and use
73
+ `pip install dccp --user`.
74
+
75
+ ## What are morphological perceptrons?
76
+
77
+ Perceptrons are the fundamental building blocks of many machine learning
78
+ constructs, namely the well-known neural networks.
79
+
80
+ They fulfill this role because their simple yet effective activation function
81
+ ($y = w \cdot x + b$) allows for the emergence of arbitrarily complex behavior,
82
+ for example when such perceptrons are wired together in networks.
83
+
84
+ In the equation above, $w$ and $x$ are row and column matrices respectively,
85
+ meaning their multiplication equates to a set of scalar multiplications,
86
+ followed by a summation of all these multiplication results.
87
+
88
+ "Morphological" perceptrons are similar to the aforedescribed "classical"
89
+ perceptrons, but differ in the fact that they generally do not have a bias $b$,
90
+ and most importantly that the operations performed are different:
91
+
92
+ While, as established above, a classical perceptron computes several
93
+ multiplications followed by a summation, a morphological perceptron performs
94
+ multiple additions, followed by a $\min$ or $\max$.
95
+
96
+ This simple shift into a different semiring creates a more complex decision
97
+ boundary, making these constructs a natural choice for designing binary
98
+ classifiers.
99
+
100
+ This repository explores the behavior of such perceptrons, namely standalone
101
+ ones, but also more complex structures, such as r-DEP and l-DEP.
@@ -4,24 +4,35 @@
4
4
  requires = ["hatchling"]
5
5
  build-backend = "hatchling.build"
6
6
 
7
+ # Production
8
+
7
9
  [project]
8
10
  name = "sklearn-morpho"
9
- version = "0.2.2"
11
+ version = "0.2.3"
12
+ requires-python = ">=3.11"
10
13
  dependencies = [
11
14
  "cvxpy>=1.9.0,<2.0.0",
12
15
  "numpy>=2.0.0",
13
16
  "scikit-learn>=1.8.0,<2.0.0",
14
17
  ]
18
+ classifiers = [
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ ]
24
+ readme = "README-PyPI.md"
15
25
 
16
26
  [project.optional-dependencies]
17
27
  dccp = [
18
28
  "dccp>=1.1.1,<2.0.0",
19
- "pytest>=9.0.0,<10.0.0",
20
29
  ]
21
30
 
22
31
  [tool.pytest.ini_options]
23
32
  testpaths = ["tests"]
24
33
 
34
+ # Dev
35
+
25
36
  [tool.hatch.envs.default]
26
37
  python = "3.13"
27
38
  path = ".venv" # for LSPs
@@ -36,7 +47,30 @@ dependencies = [
36
47
  "ruff>=0.15.0,<1.0.0", # linter
37
48
  ]
38
49
 
50
+ [tool.hatch.envs.default.env-vars]
51
+ SCIPY_ARRAY_API = "1" # prevent skip warnings in check_estimator
52
+
53
+ # Matrix test
54
+
55
+ [tool.hatch.envs.test]
56
+ template = "test" # break inheritance from default to ignore .venv
57
+ features = [
58
+ "dccp",
59
+ ]
60
+ dependencies = [
61
+ "pandas>=3.0.0",
62
+ "pytest>=9.0.0,<10.0.0",
63
+ "pytest-cov>=7.1.0,<8.0.0",
64
+ ]
65
+
66
+ [tool.hatch.envs.test.env-vars]
67
+ SCIPY_ARRAY_API = "1"
68
+
69
+ [[tool.hatch.envs.test.matrix]]
70
+ python = ["3.11", "3.12", "3.13", "3.14"]
71
+
39
72
  # Ruff
73
+
40
74
  [tool.ruff]
41
75
  indent-width = 4
42
76
  line-length = 80
@@ -1,11 +1,13 @@
1
1
  """
2
2
  Create and train different estimators and display their respective scores
3
3
  averaged from multiple datasets.
4
+
4
5
  Estimators selection inspired by arxiv/2011.06512
5
6
  """
6
7
 
7
8
  import json
8
9
  import signal
10
+ import warnings
9
11
  import numpy as np
10
12
  from time import time
11
13
  from scipy.sparse._csr import csr_matrix
@@ -24,8 +26,8 @@ from sklearn.pipeline import make_pipeline
24
26
  from sklearn.neural_network import MLPClassifier
25
27
  from sklearn.multiclass import OneVsRestClassifier
26
28
 
27
- FILE = 'comparison_data.json'
28
- n_splits = 5
29
+ FILE = 'comparison.json'
30
+ n_folds = 5
29
31
  timeout = 60
30
32
 
31
33
  # set up estimators and datasets
@@ -34,11 +36,11 @@ print(f'Random state: {random_state}')
34
36
 
35
37
  estimators = {
36
38
  'l-DEP': OneVsRestClassifier(LDEP(random_state=random_state)),
37
- 'DCCP l-DEP': OneVsRestClassifier(
39
+ 'DCCP l_DEP': OneVsRestClassifier(
38
40
  LDEP(use_dccp_library=True, random_state=random_state)
39
41
  ),
40
42
  'r-DEP': OneVsRestClassifier(RDEP(random_state=random_state)),
41
- 'DCCP r-DEP': OneVsRestClassifier(
43
+ 'DCCP r_DEP': OneVsRestClassifier(
42
44
  RDEP(use_dccp_library=True, random_state=random_state)
43
45
  ),
44
46
  'Morpho_max': OneVsRestClassifier(
@@ -133,13 +135,13 @@ signal.signal(signal.SIGALRM, timeout_handler)
133
135
  def save_data():
134
136
  with open(FILE, 'w') as f:
135
137
  json.dump(
136
- {'n_splits': n_splits, 'scores': scores, 'times': times},
138
+ {'n_folds': n_folds, 'scores': scores, 'times': times},
137
139
  f,
138
140
  indent=2,
139
141
  )
140
142
 
141
143
 
142
- skf = StratifiedKFold(n_splits=n_splits)
144
+ skf = StratifiedKFold(n_splits=n_folds)
143
145
  for dataset_name in datasets_names:
144
146
  print(f'Training with dataset "{dataset_name}"...')
145
147
  scores[dataset_name] = {}
@@ -164,22 +166,19 @@ for dataset_name in datasets_names:
164
166
  estimator,
165
167
  )
166
168
 
167
- for X_fold, y_fold in skf.split(X, y):
168
- X_train, X_test, y_train, y_test = train_test_split(
169
- X, y, test_size=0.3, random_state=random_state
170
- )
169
+ for i_train, i_test in skf.split(X, y):
170
+ X_train, X_test = X[i_train], X[i_test]
171
+ y_train, y_test = y[i_train], y[i_test]
171
172
 
172
173
  signal.alarm(timeout)
173
174
  t0 = time()
174
175
  try:
175
176
  estimator.fit(X_train, y_train)
176
177
  except TimeoutException:
177
- print(
178
- f' Warning: {estimator_name} timed out after {timeout}s.'
179
- )
178
+ warnings.warn(f'{estimator_name} timed out after {timeout}s.')
180
179
  scores[dataset_name][estimator_name].append(0)
181
180
  times[dataset_name][estimator_name].append(timeout)
182
- continue
181
+ break
183
182
 
184
183
  t1 = time()
185
184
  signal.alarm(0)
@@ -0,0 +1,368 @@
1
+ """
2
+ Create and train different estimators and display their respective scores
3
+ averaged from multiple datasets.
4
+ Train perceptrons in parallel using multiple processes, meaning timing
5
+ information (as well as scores depending on internal solver timeouts) may be
6
+ inaccurate, while training will take less time.
7
+
8
+ Because of the multiprocessing nature of this, timeouts are not asynchronous and
9
+ long-running cpp code can make processing take longer than is theoretically
10
+ permitted by the timeout.
11
+
12
+ Estimators selection inspired by arxiv/2011.06512
13
+ """
14
+
15
+ import os
16
+ import sys
17
+ import json
18
+ import time
19
+ import signal
20
+ import warnings
21
+ import numpy as np
22
+
23
+ from time import time, sleep
24
+ from threading import Thread
25
+ from multiprocessing import Manager
26
+ from joblib import Parallel, delayed
27
+ from scipy.sparse._csr import csr_matrix
28
+
29
+ from sklearn.metrics import f1_score
30
+ from sklearn.datasets import load_breast_cancer, fetch_openml
31
+ from sklearn.preprocessing import OrdinalEncoder
32
+ from sklearn.model_selection import train_test_split, StratifiedKFold
33
+
34
+ from sklearn_morpho import LDEP, RDEP, MorphoPerceptron
35
+ from sklearn.svm import LinearSVC, SVC
36
+ from sklearn.impute import SimpleImputer
37
+ from sklearn.pipeline import make_pipeline
38
+ from sklearn.neural_network import MLPClassifier
39
+ from sklearn.multiclass import OneVsRestClassifier
40
+
41
+ ## Timeouts handling
42
+
43
+
44
+ class TimeoutException(Exception):
45
+ pass
46
+
47
+
48
+ def timeout_handler(sugnum, frame):
49
+ raise TimeoutException()
50
+
51
+
52
+ ## Configuration
53
+
54
+ FILE = 'comparison.json'
55
+ # ignore warnings for cleaner logs
56
+ ignore_warnings = True
57
+ n_folds = 5
58
+ max_jobs = 15
59
+ random_state = np.random.RandomState()
60
+ LINE_SIZE = 100
61
+ timeout = 60
62
+
63
+ if ignore_warnings:
64
+ warnings.filterwarnings('ignore')
65
+
66
+ print(f'Random state: {random_state}')
67
+ print(f'Comparison data will be outputted to: "{FILE}"')
68
+
69
+ ## Set up datasets
70
+
71
+
72
+ def get_clean_openml(name: str, **kwargs) -> tuple[np.ndarray, np.ndarray]:
73
+ kwargs.setdefault('as_frame', False)
74
+ kwargs.setdefault('version', 1)
75
+ X, y = fetch_openml(name, return_X_y=True, **kwargs)
76
+
77
+ # make sure X and y are not sparse
78
+ if isinstance(X, csr_matrix):
79
+ X = X.toarray()
80
+ if isinstance(y, csr_matrix):
81
+ y = y.toarray()
82
+
83
+ # make sure X contains only numbers, not yes/no like in australian
84
+ oe = OrdinalEncoder()
85
+ X = oe.fit_transform(X)
86
+
87
+ return X, y
88
+
89
+
90
+ skf = StratifiedKFold(n_splits=n_folds)
91
+ datasets_names = [
92
+ 'acute-inflammations',
93
+ 'australian',
94
+ 'banana',
95
+ 'banknote-authentication',
96
+ 'blood-transfusion-service-center',
97
+ 'breast-cancer',
98
+ 'colic',
99
+ 'credit-approval',
100
+ 'credit-g',
101
+ 'cylinder-bands',
102
+ 'diabetes',
103
+ 'eeg-eye-state',
104
+ 'haberman',
105
+ 'hill-valley',
106
+ 'ilpd',
107
+ 'ionosphere',
108
+ 'mofn-3-7-10',
109
+ 'monks-problems-2',
110
+ 'mushroom',
111
+ 'phoneme',
112
+ 'PhishingWebsites',
113
+ 'sick',
114
+ 'sonar',
115
+ 'spambase',
116
+ 'steel-plates-fault',
117
+ 'thoracic-surgery',
118
+ 'tic-tac-toe',
119
+ 'titanic',
120
+ ]
121
+
122
+ # datasets not included compared to arxiv/2011.06512:
123
+ # - chess (nonexistent)
124
+ # - internet-advertisements (internally referring to a nonexistent dataset)
125
+
126
+ datasets_options = {
127
+ 'australian': {'version': 4},
128
+ 'Breast_Cancer_Wisconsin': {'as_frame': True},
129
+ 'cylinder-bands': {'version': 6},
130
+ 'titanic': {'as_frame': True},
131
+ }
132
+
133
+ ## Create estimators
134
+
135
+ print('Creating estimators...')
136
+ estimators = {
137
+ 'l-DEP': OneVsRestClassifier(LDEP(random_state=random_state)),
138
+ #'DCCP l_DEP': OneVsRestClassifier(
139
+ # LDEP(use_dccp_library=True, random_state=random_state)
140
+ # ),
141
+ 'r-DEP': OneVsRestClassifier(RDEP(random_state=random_state)),
142
+ #'DCCP r_DEP': OneVsRestClassifier(
143
+ # RDEP(use_dccp_library=True, random_state=random_state)
144
+ # ),
145
+ 'Morpho_max': OneVsRestClassifier(
146
+ MorphoPerceptron(kind='max', random_state=random_state)
147
+ ),
148
+ 'Morpho_min': OneVsRestClassifier(
149
+ MorphoPerceptron(kind='min', random_state=random_state)
150
+ ),
151
+ 'Linear SVC': LinearSVC(random_state=random_state),
152
+ 'RBF SVC': SVC(kernel='rbf', random_state=random_state),
153
+ 'MLP': MLPClassifier(max_iter=1000, random_state=random_state),
154
+ 'Poly SVC': SVC(kernel='poly', random_state=random_state),
155
+ }
156
+
157
+ ## Load datasets
158
+ print('\nLoading datasets...')
159
+
160
+ datasets = {}
161
+ for i, dataset_name in enumerate(datasets_names):
162
+ print(
163
+ f'\033[32m=>\033[m Loading {dataset_name:<35} '
164
+ f'({i:02}/{len(datasets_names):02})',
165
+ end='\r',
166
+ )
167
+ match dataset_name:
168
+ case 'breast-cancer':
169
+ datasets[dataset_name] = load_breast_cancer(return_X_y=True)
170
+ case _:
171
+ datasets[dataset_name] = get_clean_openml(
172
+ dataset_name, **datasets_options.get(dataset_name, {})
173
+ )
174
+ print(' ' * LINE_SIZE)
175
+
176
+ ## Main logic
177
+
178
+ # centralized data
179
+ manager = Manager()
180
+ scores = manager.dict({d: {e: [] for e in estimators} for d in datasets_names})
181
+ times = manager.dict({d: {e: [] for e in estimators} for d in datasets_names})
182
+
183
+ # the elements are a list of:
184
+ # - start timestamp, or 0 if not started
185
+ # - number from 0 to 1 for progress, or a timestamp if ended
186
+ progress_states = manager.dict(
187
+ {d: {e: [0.0, 0.0] for e in estimators} for d in datasets_names}
188
+ )
189
+ total_jobs = len(datasets) * len(estimators)
190
+
191
+
192
+ def worker(dataset_name: str, estimator_name: str) -> None:
193
+ if ignore_warnings:
194
+ warnings.filterwarnings('ignore')
195
+
196
+ score_arr = []
197
+ time_arr = []
198
+ start = time()
199
+
200
+ # need to reassign because of shared data getters/setters
201
+ temp = progress_states[dataset_name]
202
+ temp[estimator_name][0] = start
203
+ progress_states[dataset_name] = temp
204
+
205
+ X, y = datasets[dataset_name]
206
+ estimator = make_pipeline(
207
+ SimpleImputer(strategy='mean'), # remove NaNs
208
+ estimators[estimator_name],
209
+ )
210
+
211
+ for i, (i_train, i_test) in enumerate(skf.split(X, y)):
212
+ try:
213
+ signal.signal(signal.SIGALRM, timeout_handler)
214
+ signal.alarm(timeout)
215
+
216
+ X_train, X_test = X[i_train], X[i_test]
217
+ y_train, y_test = y[i_train], y[i_test]
218
+
219
+ t0 = time()
220
+ estimator.fit(X_train, y_train)
221
+ t1 = time()
222
+
223
+ score = f1_score(y_test, estimator.predict(X_test), average='micro')
224
+ score_arr.append(score)
225
+ time_arr.append(t1 - t0)
226
+
227
+ temp = progress_states[dataset_name]
228
+ temp[estimator_name][1] = (i + 1) / n_folds
229
+ progress_states[dataset_name] = temp
230
+ except TimeoutException:
231
+ break
232
+
233
+ for data_source, value in (
234
+ (scores, score_arr),
235
+ (times, time_arr),
236
+ (progress_states, [start, time()]),
237
+ ):
238
+ temp = data_source[dataset_name]
239
+ temp[estimator_name] = value
240
+ data_source[dataset_name] = temp
241
+ signal.alarm(0)
242
+
243
+
244
+ def save_data():
245
+ with open(FILE, 'w') as f:
246
+ data = {
247
+ 'n_folds': n_folds,
248
+ 'scores': dict(scores),
249
+ 'times': dict(times),
250
+ }
251
+ json.dump(data, f, indent=2)
252
+
253
+
254
+ def progress_bar():
255
+ print('=' * LINE_SIZE)
256
+ print('\n')
257
+ prev_running = 0
258
+
259
+ def format_time(t):
260
+ hm, s = divmod(int(t), 60)
261
+ h, m = divmod(hm, 60)
262
+ return f'\033[34m{h:02}:{m:02}:{s:02}\033[m'
263
+
264
+ def eta_str(progress, time_spent):
265
+ if not progress:
266
+ return f'--:--:--'
267
+
268
+ eta = time_spent / progress * (1 - progress)
269
+ return format_time(eta)
270
+
271
+ def bar(name, time_spent, progress):
272
+ print(
273
+ f'[{"#" * round(progress * 30):-<30}] '
274
+ f'(\033[33m{int(progress * 100):>3}%\033[m) '
275
+ f'{format_time(time_spent)} {name:<50}|'
276
+ )
277
+
278
+ while True:
279
+ running = []
280
+ total_time = 0
281
+ total_progress = 0
282
+ states = []
283
+ for dataset_name, elt in dict(progress_states).items():
284
+ for estimator_name, (start, progress) in elt.items():
285
+ if progress > 1:
286
+ states.append(2)
287
+ total_time += progress - start
288
+ elif start != 0:
289
+ states.append(1)
290
+ total_time += time() - start
291
+ else:
292
+ states.append(0)
293
+ if start != 0:
294
+ if progress < 1:
295
+ running.append(
296
+ (dataset_name, estimator_name, start, progress)
297
+ )
298
+ total_progress += progress
299
+ else:
300
+ total_progress += 1
301
+
302
+ total_done, total_running, total_queued = (
303
+ states.count(k) for k in [2, 1, 0]
304
+ )
305
+ total_progress /= total_jobs
306
+ running.sort(key=lambda x: x[3])
307
+ now_running = len(running)
308
+
309
+ for i in range(prev_running + 1, -1, -1):
310
+ if i < now_running:
311
+ print(end='\033[F')
312
+ else:
313
+ print(' ' * LINE_SIZE, end='\033[F')
314
+
315
+ for dataset_name, estimator_name, start, progress in running:
316
+ bar(f'{dataset_name} - {estimator_name}', time() - start, progress)
317
+
318
+ print('=' * LINE_SIZE)
319
+ print(
320
+ f'TOTAL [{"#" * round(total_progress * 24):-<24}] '
321
+ f'(\033[33m{int(total_progress * 100):>3}%\033[m) '
322
+ f'ETA: {eta_str(total_progress, total_time)} - '
323
+ f'{total_done} done, {total_running} running, {total_queued} queued'
324
+ )
325
+
326
+ # a job was just done, update the results file
327
+ if now_running != prev_running:
328
+ save_data()
329
+
330
+ if total_done == total_jobs:
331
+ break
332
+
333
+ prev_running = now_running
334
+ sleep(0.2)
335
+
336
+
337
+ ## train in parallel
338
+ print('Preparing to train')
339
+
340
+
341
+ n_jobs = min(os.process_cpu_count(), total_jobs, max_jobs)
342
+ if n_jobs <= 0:
343
+ print('Nothing to do')
344
+ sys.exit(0)
345
+
346
+ print(f'{len(datasets)} datasets to process, splitting work in {n_jobs} jobs.')
347
+ print()
348
+
349
+ progress_bar_thread = Thread(target=progress_bar)
350
+ progress_bar_thread.start()
351
+
352
+ Parallel(n_jobs=n_jobs)(
353
+ delayed(worker)(dataset_name, estimator_name)
354
+ for dataset_name in datasets
355
+ for estimator_name in estimators
356
+ )
357
+
358
+ # force end all datasets
359
+ for d in datasets:
360
+ for e in estimators:
361
+ progress_states[e][d][0] = time()
362
+ progress_states[e][d][1] = time()
363
+ progress_bar_thread.join()
364
+
365
+ scores, times = dict(scores), dict(times)
366
+
367
+ print('Done.')
368
+ save_data()
@@ -6,19 +6,25 @@ import json
6
6
  import numpy as np
7
7
  import matplotlib.pyplot as plt
8
8
 
9
- FILE = 'comparison_data.json'
9
+ FILE = 'comparison.json'
10
10
 
11
11
  with open(FILE, 'r') as f:
12
12
  data = json.load(f)
13
13
  scores = data['scores']
14
14
  times = data['times']
15
15
 
16
- n_splits = data['n_splits']
16
+ n_folds = data['n_folds']
17
17
 
18
18
  # display summary table in console
19
19
  datasets_names = list(scores.keys())
20
20
  estimators_names = list(scores[datasets_names[0]].keys())
21
21
 
22
+ for dataset_name in datasets_names:
23
+ for estimator_name in estimators_names:
24
+ for data_source in (scores, times):
25
+ arr = data_source[dataset_name][estimator_name]
26
+ data_source[dataset_name][estimator_name] = np.array(arr)
27
+
22
28
  print()
23
29
  params = (
24
30
  ('F1 score', scores, np.argmax, np.argmin),
@@ -36,7 +42,7 @@ for name, data_source, best_func, worst_func in params:
36
42
 
37
43
  dataset_res = [] # list of (avg, std)
38
44
  for estimator_name in estimators_names:
39
- res = np.array(data_source[dataset_name][estimator_name])
45
+ res = data_source[dataset_name][estimator_name]
40
46
 
41
47
  avg = np.average(res)
42
48
  std = res.std()
@@ -56,19 +62,24 @@ for name, data_source, best_func, worst_func in params:
56
62
  print(f'{dataset_name:>35}' + line)
57
63
  print()
58
64
 
59
- # merge and display results averaged over all datasets
65
+ # merge and display results averaged over all datasets where there are results
60
66
  estimators_scores = {}
61
67
  estimators_times = {}
62
68
  for estimator_name in estimators_names:
63
- estimators_scores[estimator_name] = np.empty(len(datasets_names))
64
- estimators_times[estimator_name] = np.empty(len(datasets_names))
65
-
66
- for i, dataset_name in enumerate(datasets_names):
67
- score = np.array(scores[dataset_name][estimator_name])
68
- time = np.array(times[dataset_name][estimator_name])
69
-
70
- estimators_scores[estimator_name][i] = np.average(score)
71
- estimators_times[estimator_name][i] = np.average(time)
69
+ estimators_scores[estimator_name] = np.array(
70
+ [
71
+ score
72
+ for dataset_name in datasets_names
73
+ for score in scores[dataset_name][estimator_name]
74
+ ]
75
+ )
76
+ estimators_times[estimator_name] = np.array(
77
+ [
78
+ time
79
+ for dataset_name in datasets_names
80
+ for time in times[dataset_name][estimator_name]
81
+ ]
82
+ )
72
83
 
73
84
  fig, axs = plt.subplots(ncols=2, nrows=1)
74
85
  # log scale for times
@@ -1,90 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [ "master" ]
6
- pull_request:
7
- branches: [ "**" ]
8
-
9
- jobs:
10
- test:
11
- name: Test on ${{ matrix.os }}
12
- runs-on: ${{ matrix.os }}
13
- strategy:
14
- matrix:
15
- os: [ ubuntu-latest, windows-latest, macos-latest ]
16
-
17
- steps:
18
- - name: Checkout repository
19
- uses: actions/checkout@v4
20
-
21
- - name: Set up Python
22
- uses: actions/setup-python@v5
23
- with:
24
- python-version: "3.13"
25
-
26
- - name: Install Hatch
27
- run: python -m pip install hatch
28
-
29
- - name: Run tests
30
- run: hatch run pytest
31
-
32
- coverage:
33
- name: Coverage test, ignore failed tests
34
- runs-on: ubuntu-latest
35
-
36
- steps:
37
- - name: Checkout repository
38
- uses: actions/checkout@v4
39
-
40
- - name: Set up Python
41
- uses: actions/setup-python@v5
42
- with:
43
- python-version: "3.13"
44
-
45
- - name: Install Hatch
46
- run: python -m pip install hatch
47
-
48
- - name: Install coverage utilities
49
- run: python -m pip install coverage
50
-
51
- - name: Check coverage
52
- run: |
53
- hatch run pytest --cov --cov-fail-under=90
54
-
55
- todo:
56
- name: Check for TODOs in code
57
- runs-on: ubuntu-latest
58
-
59
- steps:
60
- - name: Checkout repository
61
- uses: actions/checkout@v4
62
-
63
- - name: Scan for TODOs in the code
64
- run: ./check_todos.sh
65
-
66
- linter:
67
- name: Check for ${{ matrix.name }} errors
68
- runs-on: ubuntu-latest
69
- strategy:
70
- matrix:
71
- include:
72
- - name: "linting"
73
- command: "python -m ruff check"
74
- - name: "coding style"
75
- command: "[ -z \"$(python -m ruff format | grep reformatted)\" ]"
76
-
77
- steps:
78
- - name: Checkout repository
79
- uses: actions/checkout@v4
80
-
81
- - name: Set up Python
82
- uses: actions/setup-python@v5
83
- with:
84
- python-version: "3.13"
85
-
86
- - name: Install linter
87
- run: python -m pip install ruff
88
-
89
- - name: Run linter for coding style errors
90
- run: ${{ matrix.command }}
@@ -1,10 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: sklearn-morpho
3
- Version: 0.2.2
4
- License-File: LICENSE
5
- Requires-Dist: cvxpy<2.0.0,>=1.9.0
6
- Requires-Dist: numpy>=2.0.0
7
- Requires-Dist: scikit-learn<2.0.0,>=1.8.0
8
- Provides-Extra: dccp
9
- Requires-Dist: dccp<2.0.0,>=1.1.1; extra == 'dccp'
10
- Requires-Dist: pytest<10.0.0,>=9.0.0; extra == 'dccp'
@@ -1,56 +0,0 @@
1
- # sklearn-morpho
2
-
3
- scikit-learn estimator toolbox for morphological perceptrons.
4
-
5
- ![](https://img.shields.io/pypi/v/sklearn-morpho?style=flat-square)
6
- ![](https://img.shields.io/aur/version/python-sklearn-morpho?style=flat-square)
7
- ![](https://img.shields.io/github/actions/workflow/status/d-002/sklearn-morpho/ci.yml?style=flat-square)
8
-
9
- Current features:
10
-
11
- - Scikit-learn estimators:
12
- - Linear Dilation-Erosion Perceptron (l-DEP)
13
- - Reduced Dilation-Erosion Perceptron (r-DEP)
14
- - Simple Dilation and Erosion Morphological Perceptrons
15
- - Modular wrapper for DCCP optimization tasks with `cvxpy`
16
-
17
- File tree:
18
-
19
- - `src/sklearn_morpho`: contains the source code and a testsuite in its `tests`
20
- subdirectory.
21
- - `testing`: standalone files that use this library, may contain tests but they
22
- are not designed to be run as a CI testsuite for example.
23
- - `MREs`: standalone jupyter notebooks to showcase some of this library's
24
- features.
25
-
26
- ## Getting started
27
-
28
- Take a look at the Jupyter code examples in the
29
- [MREs](https://github.com/d-002/sklearn-morpho/tree/master/MREs) directory.
30
-
31
- ## Running the project
32
-
33
- Install Python 3 and hatch.
34
- Then run one of these commands:
35
-
36
- - `hatch run jupyter lab` to run the Jupyter notebooks
37
- - `hatch run pytest` for tests
38
- - `hatch shell` to run testing files like `testing/display_boundary.py` in the
39
- right environment.
40
-
41
- Special note for the estimators comparison testing files: they are split in two
42
- files to avoid training the estimators every time one wants to view the results.
43
-
44
- ## For Arch users
45
-
46
- The package is available in the
47
- [Arch User Repository](https://aur.archlinux.org) as `python-sklearn-morpho`, as
48
- are all its dependencies not already in the official packages, except for
49
- `dccp`.
50
-
51
- Since the latter is an optional dependency, you can either:
52
-
53
- - Download `dccp` through pip or use `pip install sklearn-morpho[dccp]` in a
54
- Python virtual environment.
55
- - Download `python-sklearn-morpho` from the AUR and use
56
- `pip install dccp --user`.
File without changes