ikn-library 0.1.0__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.
- ikn_library-0.1.0/.github/workflows/ci.yml +31 -0
- ikn_library-0.1.0/.github/workflows/publish.yml +60 -0
- ikn_library-0.1.0/.gitignore +10 -0
- ikn_library-0.1.0/LICENSE +21 -0
- ikn_library-0.1.0/PKG-INFO +120 -0
- ikn_library-0.1.0/README.md +97 -0
- ikn_library-0.1.0/examples/feature_selection.py +29 -0
- ikn_library-0.1.0/examples/run_aco.py +12 -0
- ikn_library-0.1.0/pyproject.toml +47 -0
- ikn_library-0.1.0/src/ikn_library/__init__.py +18 -0
- ikn_library-0.1.0/src/ikn_library/algorithms/__init__.py +7 -0
- ikn_library-0.1.0/src/ikn_library/algorithms/aco.py +76 -0
- ikn_library-0.1.0/src/ikn_library/algorithms/algorithm.py +54 -0
- ikn_library-0.1.0/src/ikn_library/algorithms/binary_aco.py +73 -0
- ikn_library-0.1.0/src/ikn_library/problems/__init__.py +7 -0
- ikn_library-0.1.0/src/ikn_library/problems/benchmarks.py +41 -0
- ikn_library-0.1.0/src/ikn_library/problems/feature_selection.py +86 -0
- ikn_library-0.1.0/src/ikn_library/problems/problem.py +52 -0
- ikn_library-0.1.0/src/ikn_library/task.py +92 -0
- ikn_library-0.1.0/tests/test_aco.py +54 -0
- ikn_library-0.1.0/tests/test_binary_aco.py +74 -0
- ikn_library-0.1.0/tests/test_feature_selection.py +62 -0
- ikn_library-0.1.0/tests/test_problems.py +32 -0
- ikn_library-0.1.0/tests/test_task.py +44 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install package with dev dependencies
|
|
25
|
+
run: pip install -e ".[dev]"
|
|
26
|
+
|
|
27
|
+
- name: Lint with ruff
|
|
28
|
+
run: ruff check src tests
|
|
29
|
+
|
|
30
|
+
- name: Run tests
|
|
31
|
+
run: pytest
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
|
|
17
|
+
- name: Build distributions
|
|
18
|
+
run: |
|
|
19
|
+
pip install build
|
|
20
|
+
python -m build
|
|
21
|
+
|
|
22
|
+
- uses: actions/upload-artifact@v4
|
|
23
|
+
with:
|
|
24
|
+
name: dist
|
|
25
|
+
path: dist/
|
|
26
|
+
|
|
27
|
+
publish:
|
|
28
|
+
needs: build
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
environment:
|
|
31
|
+
name: pypi
|
|
32
|
+
url: https://pypi.org/p/ikn-library
|
|
33
|
+
permissions:
|
|
34
|
+
id-token: write # required for PyPI Trusted Publishing (OIDC)
|
|
35
|
+
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/download-artifact@v4
|
|
38
|
+
with:
|
|
39
|
+
name: dist
|
|
40
|
+
path: dist/
|
|
41
|
+
|
|
42
|
+
- name: Publish to PyPI
|
|
43
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
44
|
+
|
|
45
|
+
github-release:
|
|
46
|
+
needs: publish
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
permissions:
|
|
49
|
+
contents: write
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/download-artifact@v4
|
|
52
|
+
with:
|
|
53
|
+
name: dist
|
|
54
|
+
path: dist/
|
|
55
|
+
|
|
56
|
+
- name: Create GitHub Release
|
|
57
|
+
uses: softprops/action-gh-release@v2
|
|
58
|
+
with:
|
|
59
|
+
files: dist/*
|
|
60
|
+
generate_release_notes: true
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Isman Kurniawan
|
|
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.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ikn-library
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Nature-inspired metaheuristic algorithms for feature selection and parameter optimization
|
|
5
|
+
Project-URL: Homepage, https://github.com/ismankrn/ikn-library
|
|
6
|
+
Author-email: Isman Kurniawan <isman.krn@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: ant-colony-optimization,feature-selection,hyperparameter-optimization,metaheuristic,optimization
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Requires-Dist: numpy>=1.24
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
19
|
+
Requires-Dist: scikit-learn>=1.3; extra == 'dev'
|
|
20
|
+
Provides-Extra: ml
|
|
21
|
+
Requires-Dist: scikit-learn>=1.3; extra == 'ml'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# ikn-library
|
|
25
|
+
|
|
26
|
+
Nature-inspired metaheuristic algorithms for continuous optimization, feature
|
|
27
|
+
selection, and parameter optimization — focusing on algorithms not yet
|
|
28
|
+
available in [NiaPy](https://github.com/NiaOrg/NiaPy), starting with
|
|
29
|
+
**Ant Colony Optimization for continuous domains (ACO-R)**.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install ikn-library
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or from source (development mode):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install -e ".[dev]"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from ikn_library import Task
|
|
47
|
+
from ikn_library.problems import Sphere
|
|
48
|
+
from ikn_library.algorithms import AntColonyOptimization
|
|
49
|
+
|
|
50
|
+
task = Task(problem=Sphere(dimension=10), max_evals=10000)
|
|
51
|
+
algo = AntColonyOptimization(population_size=30, archive_size=50, seed=42)
|
|
52
|
+
best_x, best_fitness = algo.run(task)
|
|
53
|
+
|
|
54
|
+
print("Best fitness:", best_fitness)
|
|
55
|
+
print("Best solution:", best_x)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Custom problems
|
|
59
|
+
|
|
60
|
+
Subclass `Problem` and implement `_evaluate` — for example, a
|
|
61
|
+
cross-validation score for hyperparameter optimization:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import numpy as np
|
|
65
|
+
from ikn_library.problems import Problem
|
|
66
|
+
|
|
67
|
+
class MyProblem(Problem):
|
|
68
|
+
def __init__(self, dimension=10):
|
|
69
|
+
super().__init__(dimension, lower=-10.0, upper=10.0)
|
|
70
|
+
|
|
71
|
+
def _evaluate(self, x):
|
|
72
|
+
return float(np.sum(np.abs(x)))
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Use `OptimizationType.MAXIMIZATION` in the `Task` when higher is better
|
|
76
|
+
(e.g. accuracy).
|
|
77
|
+
|
|
78
|
+
## Feature selection
|
|
79
|
+
|
|
80
|
+
Wrapper-based feature selection with a scikit-learn estimator
|
|
81
|
+
(`pip install ikn-library[ml]`):
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from sklearn.datasets import load_breast_cancer
|
|
85
|
+
|
|
86
|
+
from ikn_library import Task
|
|
87
|
+
from ikn_library.problems import FeatureSelectionProblem
|
|
88
|
+
from ikn_library.algorithms import BinaryAntColonyOptimization
|
|
89
|
+
|
|
90
|
+
X, y = load_breast_cancer(return_X_y=True)
|
|
91
|
+
problem = FeatureSelectionProblem(X, y, cv=5, alpha=0.99)
|
|
92
|
+
task = Task(problem=problem, max_evals=1000)
|
|
93
|
+
algo = BinaryAntColonyOptimization(population_size=20, seed=42)
|
|
94
|
+
best_x, best_fitness = algo.run(task)
|
|
95
|
+
|
|
96
|
+
print("Selected features:", problem.selected_features(best_x))
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The fitness balances the cross-validated score against the subset size:
|
|
100
|
+
`alpha * (1 - cv_score) + (1 - alpha) * n_selected / n_features`.
|
|
101
|
+
|
|
102
|
+
## Algorithms
|
|
103
|
+
|
|
104
|
+
| Algorithm | Class | Domain | Reference |
|
|
105
|
+
|---|---|---|---|
|
|
106
|
+
| Ant Colony Optimization (ACO-R) | `AntColonyOptimization` | continuous | Socha & Dorigo, EJOR 185(3), 2008 |
|
|
107
|
+
| Binary Ant Colony Optimization | `BinaryAntColonyOptimization` | binary / subsets | hyper-cube pheromone update |
|
|
108
|
+
|
|
109
|
+
More algorithms are planned.
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pip install -e ".[dev]"
|
|
115
|
+
pytest
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# ikn-library
|
|
2
|
+
|
|
3
|
+
Nature-inspired metaheuristic algorithms for continuous optimization, feature
|
|
4
|
+
selection, and parameter optimization — focusing on algorithms not yet
|
|
5
|
+
available in [NiaPy](https://github.com/NiaOrg/NiaPy), starting with
|
|
6
|
+
**Ant Colony Optimization for continuous domains (ACO-R)**.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install ikn-library
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or from source (development mode):
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install -e ".[dev]"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from ikn_library import Task
|
|
24
|
+
from ikn_library.problems import Sphere
|
|
25
|
+
from ikn_library.algorithms import AntColonyOptimization
|
|
26
|
+
|
|
27
|
+
task = Task(problem=Sphere(dimension=10), max_evals=10000)
|
|
28
|
+
algo = AntColonyOptimization(population_size=30, archive_size=50, seed=42)
|
|
29
|
+
best_x, best_fitness = algo.run(task)
|
|
30
|
+
|
|
31
|
+
print("Best fitness:", best_fitness)
|
|
32
|
+
print("Best solution:", best_x)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Custom problems
|
|
36
|
+
|
|
37
|
+
Subclass `Problem` and implement `_evaluate` — for example, a
|
|
38
|
+
cross-validation score for hyperparameter optimization:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import numpy as np
|
|
42
|
+
from ikn_library.problems import Problem
|
|
43
|
+
|
|
44
|
+
class MyProblem(Problem):
|
|
45
|
+
def __init__(self, dimension=10):
|
|
46
|
+
super().__init__(dimension, lower=-10.0, upper=10.0)
|
|
47
|
+
|
|
48
|
+
def _evaluate(self, x):
|
|
49
|
+
return float(np.sum(np.abs(x)))
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Use `OptimizationType.MAXIMIZATION` in the `Task` when higher is better
|
|
53
|
+
(e.g. accuracy).
|
|
54
|
+
|
|
55
|
+
## Feature selection
|
|
56
|
+
|
|
57
|
+
Wrapper-based feature selection with a scikit-learn estimator
|
|
58
|
+
(`pip install ikn-library[ml]`):
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from sklearn.datasets import load_breast_cancer
|
|
62
|
+
|
|
63
|
+
from ikn_library import Task
|
|
64
|
+
from ikn_library.problems import FeatureSelectionProblem
|
|
65
|
+
from ikn_library.algorithms import BinaryAntColonyOptimization
|
|
66
|
+
|
|
67
|
+
X, y = load_breast_cancer(return_X_y=True)
|
|
68
|
+
problem = FeatureSelectionProblem(X, y, cv=5, alpha=0.99)
|
|
69
|
+
task = Task(problem=problem, max_evals=1000)
|
|
70
|
+
algo = BinaryAntColonyOptimization(population_size=20, seed=42)
|
|
71
|
+
best_x, best_fitness = algo.run(task)
|
|
72
|
+
|
|
73
|
+
print("Selected features:", problem.selected_features(best_x))
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The fitness balances the cross-validated score against the subset size:
|
|
77
|
+
`alpha * (1 - cv_score) + (1 - alpha) * n_selected / n_features`.
|
|
78
|
+
|
|
79
|
+
## Algorithms
|
|
80
|
+
|
|
81
|
+
| Algorithm | Class | Domain | Reference |
|
|
82
|
+
|---|---|---|---|
|
|
83
|
+
| Ant Colony Optimization (ACO-R) | `AntColonyOptimization` | continuous | Socha & Dorigo, EJOR 185(3), 2008 |
|
|
84
|
+
| Binary Ant Colony Optimization | `BinaryAntColonyOptimization` | binary / subsets | hyper-cube pheromone update |
|
|
85
|
+
|
|
86
|
+
More algorithms are planned.
|
|
87
|
+
|
|
88
|
+
## Development
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pip install -e ".[dev]"
|
|
92
|
+
pytest
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Example: feature selection on the breast-cancer dataset with Binary ACO.
|
|
2
|
+
|
|
3
|
+
Requires scikit-learn: pip install ikn-library[ml]
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from sklearn.datasets import load_breast_cancer
|
|
7
|
+
from sklearn.model_selection import cross_val_score
|
|
8
|
+
from sklearn.neighbors import KNeighborsClassifier
|
|
9
|
+
|
|
10
|
+
from ikn_library import Task
|
|
11
|
+
from ikn_library.problems import FeatureSelectionProblem
|
|
12
|
+
from ikn_library.algorithms import BinaryAntColonyOptimization
|
|
13
|
+
|
|
14
|
+
data = load_breast_cancer()
|
|
15
|
+
X, y = data.data, data.target
|
|
16
|
+
estimator = KNeighborsClassifier(n_neighbors=5)
|
|
17
|
+
|
|
18
|
+
problem = FeatureSelectionProblem(X, y, estimator=estimator, cv=5, alpha=0.99)
|
|
19
|
+
task = Task(problem=problem, max_evals=1000)
|
|
20
|
+
algo = BinaryAntColonyOptimization(population_size=20, evaporation=0.1, seed=42)
|
|
21
|
+
best_x, best_fitness = algo.run(task)
|
|
22
|
+
|
|
23
|
+
selected = problem.selected_features(best_x)
|
|
24
|
+
baseline = cross_val_score(estimator, X, y, cv=5).mean()
|
|
25
|
+
score = cross_val_score(estimator, X[:, selected], y, cv=5).mean()
|
|
26
|
+
|
|
27
|
+
print(f"All {X.shape[1]} features : accuracy = {baseline:.4f}")
|
|
28
|
+
print(f"Selected {len(selected)} features: accuracy = {score:.4f}")
|
|
29
|
+
print("Selected feature names:", list(data.feature_names[selected]))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Example: minimize benchmark functions with Ant Colony Optimization."""
|
|
2
|
+
|
|
3
|
+
from ikn_library import Task
|
|
4
|
+
from ikn_library.problems import Ackley, Rastrigin, Sphere
|
|
5
|
+
from ikn_library.algorithms import AntColonyOptimization
|
|
6
|
+
|
|
7
|
+
for problem_cls in (Sphere, Rastrigin, Ackley):
|
|
8
|
+
task = Task(problem=problem_cls(dimension=10), max_evals=20000)
|
|
9
|
+
algo = AntColonyOptimization(population_size=30, archive_size=50, seed=42)
|
|
10
|
+
best_x, best_fitness = algo.run(task)
|
|
11
|
+
print(f"{problem_cls.__name__:>10}: best fitness = {best_fitness:.6g} "
|
|
12
|
+
f"({task.evals} evals, {task.iters} iters)")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ikn-library"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Nature-inspired metaheuristic algorithms for feature selection and parameter optimization"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [{ name = "Isman Kurniawan", email = "isman.krn@gmail.com" }]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
keywords = [
|
|
14
|
+
"optimization",
|
|
15
|
+
"metaheuristic",
|
|
16
|
+
"ant-colony-optimization",
|
|
17
|
+
"feature-selection",
|
|
18
|
+
"hyperparameter-optimization",
|
|
19
|
+
]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 3 - Alpha",
|
|
22
|
+
"Intended Audience :: Science/Research",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"numpy>=1.24",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
ml = [
|
|
32
|
+
"scikit-learn>=1.3",
|
|
33
|
+
]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=7.0",
|
|
36
|
+
"ruff",
|
|
37
|
+
"scikit-learn>=1.3",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/ismankrn/ikn-library"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/ikn_library"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""ikn_library: nature-inspired metaheuristic algorithms.
|
|
2
|
+
|
|
3
|
+
Metaheuristic algorithms for continuous optimization, feature selection,
|
|
4
|
+
and parameter optimization, following a NiaPy-like workflow:
|
|
5
|
+
|
|
6
|
+
>>> from ikn_library import Task
|
|
7
|
+
>>> from ikn_library.problems import Sphere
|
|
8
|
+
>>> from ikn_library.algorithms import AntColonyOptimization
|
|
9
|
+
>>> task = Task(problem=Sphere(dimension=10), max_evals=10000)
|
|
10
|
+
>>> algo = AntColonyOptimization(population_size=30, seed=42)
|
|
11
|
+
>>> best_x, best_fitness = algo.run(task)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from ikn_library.task import OptimizationType, Task
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
|
|
18
|
+
__all__ = ["OptimizationType", "Task", "__version__"]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Metaheuristic algorithms."""
|
|
2
|
+
|
|
3
|
+
from ikn_library.algorithms.aco import AntColonyOptimization
|
|
4
|
+
from ikn_library.algorithms.algorithm import Algorithm
|
|
5
|
+
from ikn_library.algorithms.binary_aco import BinaryAntColonyOptimization
|
|
6
|
+
|
|
7
|
+
__all__ = ["Algorithm", "AntColonyOptimization", "BinaryAntColonyOptimization"]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Ant Colony Optimization for continuous domains (ACO-R).
|
|
2
|
+
|
|
3
|
+
Reference:
|
|
4
|
+
K. Socha and M. Dorigo, "Ant colony optimization for continuous
|
|
5
|
+
domains," European Journal of Operational Research, 185(3), 2008.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from ikn_library.algorithms.algorithm import Algorithm
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AntColonyOptimization(Algorithm):
|
|
14
|
+
"""ACO-R: Ant Colony Optimization for continuous search spaces.
|
|
15
|
+
|
|
16
|
+
Keeps an archive of the best solutions found so far. Each ant builds
|
|
17
|
+
a new solution by picking a guide solution from the archive (better
|
|
18
|
+
solutions are picked with higher probability) and sampling each
|
|
19
|
+
coordinate from a Gaussian centered on the guide. The Gaussian width
|
|
20
|
+
shrinks as the archive converges, balancing exploration and
|
|
21
|
+
exploitation — the archive plays the role of the pheromone trail.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
population_size: Number of ants (new solutions) per iteration.
|
|
25
|
+
archive_size: Number of solutions kept in the archive (k).
|
|
26
|
+
intensification: Locality of search (q). Small values focus on
|
|
27
|
+
the best archive solutions; larger values spread the
|
|
28
|
+
selection more evenly.
|
|
29
|
+
evaporation: Speed of convergence (xi). Plays a role similar to
|
|
30
|
+
pheromone evaporation: higher values mean slower convergence.
|
|
31
|
+
seed: Random seed for reproducibility.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, population_size=30, archive_size=50,
|
|
35
|
+
intensification=0.1, evaporation=0.85, seed=None):
|
|
36
|
+
super().__init__(population_size=population_size, seed=seed)
|
|
37
|
+
if archive_size < 2:
|
|
38
|
+
raise ValueError("archive_size must be >= 2")
|
|
39
|
+
self.archive_size = int(archive_size)
|
|
40
|
+
self.intensification = float(intensification)
|
|
41
|
+
self.evaporation = float(evaporation)
|
|
42
|
+
|
|
43
|
+
def init_population(self, task):
|
|
44
|
+
archive = self.rng.uniform(
|
|
45
|
+
task.lower, task.upper, (self.archive_size, task.dimension)
|
|
46
|
+
)
|
|
47
|
+
fitness = np.array([task.eval(x) for x in archive])
|
|
48
|
+
order = np.argsort(fitness)
|
|
49
|
+
return archive[order], fitness[order]
|
|
50
|
+
|
|
51
|
+
def _selection_weights(self):
|
|
52
|
+
k, q = self.archive_size, self.intensification
|
|
53
|
+
ranks = np.arange(k)
|
|
54
|
+
weights = np.exp(-(ranks ** 2) / (2.0 * (q * k) ** 2)) / (q * k * np.sqrt(2.0 * np.pi))
|
|
55
|
+
return weights / np.sum(weights)
|
|
56
|
+
|
|
57
|
+
def run_iteration(self, task, state):
|
|
58
|
+
archive, fitness = state
|
|
59
|
+
probabilities = self._selection_weights()
|
|
60
|
+
|
|
61
|
+
ants = np.empty((self.population_size, task.dimension))
|
|
62
|
+
for a in range(self.population_size):
|
|
63
|
+
guide = self.rng.choice(self.archive_size, p=probabilities)
|
|
64
|
+
# Gaussian width per dimension: mean distance from the guide
|
|
65
|
+
# to the rest of the archive, scaled by the evaporation rate.
|
|
66
|
+
sigma = self.evaporation * np.sum(
|
|
67
|
+
np.abs(archive - archive[guide]), axis=0
|
|
68
|
+
) / (self.archive_size - 1)
|
|
69
|
+
ants[a] = task.repair(self.rng.normal(archive[guide], np.maximum(sigma, 1e-12)))
|
|
70
|
+
ant_fitness = np.array([task.eval(x) for x in ants])
|
|
71
|
+
|
|
72
|
+
# Merge ants into the archive and keep the best archive_size solutions.
|
|
73
|
+
merged = np.vstack([archive, ants])
|
|
74
|
+
merged_fitness = np.concatenate([fitness, ant_fitness])
|
|
75
|
+
order = np.argsort(merged_fitness)[: self.archive_size]
|
|
76
|
+
return merged[order], merged_fitness[order]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Base class for metaheuristic algorithms."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Algorithm:
|
|
7
|
+
"""Base class for population-based metaheuristic algorithms.
|
|
8
|
+
|
|
9
|
+
Subclasses implement :meth:`init_population` and :meth:`run_iteration`;
|
|
10
|
+
the shared :meth:`run` loop handles the budget and result reporting.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
population_size: Number of individuals in the population.
|
|
14
|
+
seed: Random seed for reproducibility (optional).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, population_size=25, seed=None):
|
|
18
|
+
if population_size < 1:
|
|
19
|
+
raise ValueError("population_size must be >= 1")
|
|
20
|
+
self.population_size = int(population_size)
|
|
21
|
+
self.rng = np.random.default_rng(seed)
|
|
22
|
+
|
|
23
|
+
def run(self, task):
|
|
24
|
+
"""Run the algorithm on ``task`` until its budget is exhausted.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
tuple: ``(best_x, best_fitness)``.
|
|
28
|
+
"""
|
|
29
|
+
state = self.init_population(task)
|
|
30
|
+
task.next_iter()
|
|
31
|
+
while not task.stopping_condition():
|
|
32
|
+
state = self.run_iteration(task, state)
|
|
33
|
+
task.next_iter()
|
|
34
|
+
return task.result()
|
|
35
|
+
|
|
36
|
+
def init_population(self, task):
|
|
37
|
+
"""Create the initial population; return the algorithm state.
|
|
38
|
+
|
|
39
|
+
The default creates ``population_size`` uniform random solutions
|
|
40
|
+
and returns ``(population, fitness)`` arrays.
|
|
41
|
+
"""
|
|
42
|
+
population = self.rng.uniform(
|
|
43
|
+
task.lower, task.upper, (self.population_size, task.dimension)
|
|
44
|
+
)
|
|
45
|
+
fitness = np.array([task.eval(x) for x in population])
|
|
46
|
+
return population, fitness
|
|
47
|
+
|
|
48
|
+
def run_iteration(self, task, state):
|
|
49
|
+
"""Perform one iteration; receive and return the algorithm state."""
|
|
50
|
+
raise NotImplementedError
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def name(self):
|
|
54
|
+
return type(self).__name__
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Binary Ant Colony Optimization for subset-selection problems.
|
|
2
|
+
|
|
3
|
+
Each decision variable is a bit (1 = selected, 0 = not selected). A
|
|
4
|
+
pheromone value is maintained per (variable, bit-value) pair; ants build
|
|
5
|
+
bit strings by sampling each bit with a probability proportional to its
|
|
6
|
+
pheromone. Pheromone is updated in the hyper-cube framework: it
|
|
7
|
+
evaporates toward the bit values of the best solution found so far, and
|
|
8
|
+
is clamped to ``[tau_min, tau_max]`` to preserve exploration.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from ikn_library.algorithms.algorithm import Algorithm
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BinaryAntColonyOptimization(Algorithm):
|
|
17
|
+
"""Binary ACO for feature selection and other subset problems.
|
|
18
|
+
|
|
19
|
+
Solutions are 0/1 vectors, so the wrapped problem must interpret its
|
|
20
|
+
input as a bit mask (e.g.
|
|
21
|
+
:class:`~ikn_library.problems.FeatureSelectionProblem`).
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
population_size: Number of ants per iteration.
|
|
25
|
+
evaporation: Pheromone evaporation/learning rate (rho) in (0, 1).
|
|
26
|
+
Higher values converge faster toward the best solution.
|
|
27
|
+
alpha: Pheromone importance exponent.
|
|
28
|
+
tau_min: Lower pheromone limit, keeps every bit reachable.
|
|
29
|
+
tau_max: Upper pheromone limit.
|
|
30
|
+
seed: Random seed for reproducibility.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, population_size=30, evaporation=0.1, alpha=1.0,
|
|
34
|
+
tau_min=0.1, tau_max=0.9, seed=None):
|
|
35
|
+
super().__init__(population_size=population_size, seed=seed)
|
|
36
|
+
if not 0.0 < evaporation < 1.0:
|
|
37
|
+
raise ValueError("evaporation must be in (0, 1)")
|
|
38
|
+
if not 0.0 < tau_min < tau_max:
|
|
39
|
+
raise ValueError("require 0 < tau_min < tau_max")
|
|
40
|
+
self.evaporation = float(evaporation)
|
|
41
|
+
self.alpha = float(alpha)
|
|
42
|
+
self.tau_min = float(tau_min)
|
|
43
|
+
self.tau_max = float(tau_max)
|
|
44
|
+
|
|
45
|
+
def _sample_ants(self, task, pheromone):
|
|
46
|
+
weights = pheromone ** self.alpha
|
|
47
|
+
p_one = weights[:, 1] / np.sum(weights, axis=1)
|
|
48
|
+
ants = (self.rng.random((self.population_size, task.dimension)) < p_one).astype(float)
|
|
49
|
+
# An all-zero ant selects nothing and is unevaluable as a subset;
|
|
50
|
+
# repair it by switching one random bit on.
|
|
51
|
+
for ant in ants:
|
|
52
|
+
if not ant.any():
|
|
53
|
+
ant[self.rng.integers(task.dimension)] = 1.0
|
|
54
|
+
return ants
|
|
55
|
+
|
|
56
|
+
def init_population(self, task):
|
|
57
|
+
pheromone = np.full((task.dimension, 2), 0.5)
|
|
58
|
+
ants = self._sample_ants(task, pheromone)
|
|
59
|
+
for ant in ants:
|
|
60
|
+
task.eval(ant)
|
|
61
|
+
return pheromone
|
|
62
|
+
|
|
63
|
+
def run_iteration(self, task, pheromone):
|
|
64
|
+
ants = self._sample_ants(task, pheromone)
|
|
65
|
+
for ant in ants:
|
|
66
|
+
task.eval(ant)
|
|
67
|
+
|
|
68
|
+
# Hyper-cube pheromone update toward the best-so-far solution.
|
|
69
|
+
best_bits = task.best_x.astype(int)
|
|
70
|
+
deposit = np.zeros_like(pheromone)
|
|
71
|
+
deposit[np.arange(task.dimension), best_bits] = 1.0
|
|
72
|
+
pheromone = (1.0 - self.evaporation) * pheromone + self.evaporation * deposit
|
|
73
|
+
return np.clip(pheromone, self.tau_min, self.tau_max)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Optimization problems: base class, benchmarks, and feature selection."""
|
|
2
|
+
|
|
3
|
+
from ikn_library.problems.benchmarks import Ackley, Rastrigin, Sphere
|
|
4
|
+
from ikn_library.problems.feature_selection import FeatureSelectionProblem
|
|
5
|
+
from ikn_library.problems.problem import Problem
|
|
6
|
+
|
|
7
|
+
__all__ = ["Ackley", "FeatureSelectionProblem", "Problem", "Rastrigin", "Sphere"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Standard benchmark functions for testing algorithms.
|
|
2
|
+
|
|
3
|
+
All benchmarks are minimization problems with a known global optimum of 0.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ikn_library.problems.problem import Problem
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Sphere(Problem):
|
|
12
|
+
"""Sphere function: ``f(x) = sum(x_i^2)``. Optimum ``f(0) = 0``."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, dimension=10, lower=-5.12, upper=5.12):
|
|
15
|
+
super().__init__(dimension, lower, upper)
|
|
16
|
+
|
|
17
|
+
def _evaluate(self, x):
|
|
18
|
+
return np.sum(x ** 2)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Rastrigin(Problem):
|
|
22
|
+
"""Rastrigin function, highly multimodal. Optimum ``f(0) = 0``."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, dimension=10, lower=-5.12, upper=5.12):
|
|
25
|
+
super().__init__(dimension, lower, upper)
|
|
26
|
+
|
|
27
|
+
def _evaluate(self, x):
|
|
28
|
+
return 10.0 * self.dimension + np.sum(x ** 2 - 10.0 * np.cos(2.0 * np.pi * x))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Ackley(Problem):
|
|
32
|
+
"""Ackley function, multimodal with a nearly flat outer region. Optimum ``f(0) = 0``."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, dimension=10, lower=-32.768, upper=32.768):
|
|
35
|
+
super().__init__(dimension, lower, upper)
|
|
36
|
+
|
|
37
|
+
def _evaluate(self, x):
|
|
38
|
+
n = self.dimension
|
|
39
|
+
term1 = -20.0 * np.exp(-0.2 * np.sqrt(np.sum(x ** 2) / n))
|
|
40
|
+
term2 = -np.exp(np.sum(np.cos(2.0 * np.pi * x)) / n)
|
|
41
|
+
return term1 + term2 + 20.0 + np.e
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Wrapper-based feature selection as an optimization problem."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from ikn_library.problems.problem import Problem
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FeatureSelectionProblem(Problem):
|
|
9
|
+
"""Wrapper feature selection: pick the feature subset that maximizes
|
|
10
|
+
a cross-validated model score while keeping the subset small.
|
|
11
|
+
|
|
12
|
+
Solutions are vectors in ``[0, 1]``; entries above ``threshold`` mark
|
|
13
|
+
selected features, so both binary algorithms (which emit 0/1 bits)
|
|
14
|
+
and continuous algorithms can optimize this problem.
|
|
15
|
+
|
|
16
|
+
The fitness (minimized) is::
|
|
17
|
+
|
|
18
|
+
alpha * (1 - cv_score) + (1 - alpha) * n_selected / n_features
|
|
19
|
+
|
|
20
|
+
Requires scikit-learn (``pip install ikn-library[ml]``).
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
X: Feature matrix of shape ``(n_samples, n_features)``.
|
|
24
|
+
y: Target vector of shape ``(n_samples,)``.
|
|
25
|
+
estimator: A scikit-learn estimator. Defaults to
|
|
26
|
+
``KNeighborsClassifier(n_neighbors=5)``, a common choice in
|
|
27
|
+
wrapper feature-selection studies.
|
|
28
|
+
cv: Number of cross-validation folds.
|
|
29
|
+
scoring: scikit-learn scoring name (e.g. ``"accuracy"``, ``"f1"``).
|
|
30
|
+
alpha: Trade-off between score quality and subset size, in [0, 1].
|
|
31
|
+
Values near 1 prioritize the model score.
|
|
32
|
+
threshold: Cut-off above which a variable counts as selected.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, X, y, estimator=None, cv=5, scoring="accuracy",
|
|
36
|
+
alpha=0.99, threshold=0.5):
|
|
37
|
+
try:
|
|
38
|
+
from sklearn.model_selection import cross_val_score # noqa: F401
|
|
39
|
+
except ImportError as exc:
|
|
40
|
+
raise ImportError(
|
|
41
|
+
"FeatureSelectionProblem requires scikit-learn; "
|
|
42
|
+
"install it with: pip install ikn-library[ml]"
|
|
43
|
+
) from exc
|
|
44
|
+
|
|
45
|
+
self.X = np.asarray(X)
|
|
46
|
+
self.y = np.asarray(y)
|
|
47
|
+
if self.X.ndim != 2:
|
|
48
|
+
raise ValueError("X must be 2-dimensional (n_samples, n_features)")
|
|
49
|
+
if len(self.X) != len(self.y):
|
|
50
|
+
raise ValueError("X and y must have the same number of samples")
|
|
51
|
+
if not 0.0 <= alpha <= 1.0:
|
|
52
|
+
raise ValueError("alpha must be in [0, 1]")
|
|
53
|
+
|
|
54
|
+
super().__init__(dimension=self.X.shape[1], lower=0.0, upper=1.0)
|
|
55
|
+
|
|
56
|
+
if estimator is None:
|
|
57
|
+
from sklearn.neighbors import KNeighborsClassifier
|
|
58
|
+
estimator = KNeighborsClassifier(n_neighbors=5)
|
|
59
|
+
self.estimator = estimator
|
|
60
|
+
self.cv = cv
|
|
61
|
+
self.scoring = scoring
|
|
62
|
+
self.alpha = float(alpha)
|
|
63
|
+
self.threshold = float(threshold)
|
|
64
|
+
|
|
65
|
+
def feature_mask(self, x):
|
|
66
|
+
"""Boolean mask of selected features for a solution vector."""
|
|
67
|
+
return np.asarray(x, dtype=float) > self.threshold
|
|
68
|
+
|
|
69
|
+
def selected_features(self, x):
|
|
70
|
+
"""Indices of the features selected by a solution vector."""
|
|
71
|
+
return np.flatnonzero(self.feature_mask(x))
|
|
72
|
+
|
|
73
|
+
def _evaluate(self, x):
|
|
74
|
+
from sklearn.base import clone
|
|
75
|
+
from sklearn.model_selection import cross_val_score
|
|
76
|
+
|
|
77
|
+
mask = self.feature_mask(x)
|
|
78
|
+
n_selected = int(np.sum(mask))
|
|
79
|
+
if n_selected == 0:
|
|
80
|
+
return 1.0 # worst possible fitness: nothing selected
|
|
81
|
+
score = cross_val_score(
|
|
82
|
+
clone(self.estimator), self.X[:, mask], self.y,
|
|
83
|
+
cv=self.cv, scoring=self.scoring,
|
|
84
|
+
).mean()
|
|
85
|
+
return (self.alpha * (1.0 - score)
|
|
86
|
+
+ (1.0 - self.alpha) * n_selected / self.dimension)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Base class for optimization problems."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Problem:
|
|
7
|
+
"""An optimization problem defined on a box-constrained search space.
|
|
8
|
+
|
|
9
|
+
Subclass this and implement :meth:`_evaluate` to define a custom
|
|
10
|
+
problem (e.g. a machine-learning objective for parameter optimization
|
|
11
|
+
or a wrapper objective for feature selection).
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
dimension: Number of decision variables.
|
|
15
|
+
lower: Lower bound(s) of the search space. Scalar or array of
|
|
16
|
+
shape ``(dimension,)``.
|
|
17
|
+
upper: Upper bound(s) of the search space. Scalar or array of
|
|
18
|
+
shape ``(dimension,)``.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
>>> class MyProblem(Problem):
|
|
22
|
+
... def __init__(self, dimension):
|
|
23
|
+
... super().__init__(dimension, lower=-10.0, upper=10.0)
|
|
24
|
+
... def _evaluate(self, x):
|
|
25
|
+
... return float(np.sum(x ** 2))
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, dimension, lower=-1.0, upper=1.0):
|
|
29
|
+
if dimension < 1:
|
|
30
|
+
raise ValueError("dimension must be >= 1")
|
|
31
|
+
self.dimension = int(dimension)
|
|
32
|
+
self.lower = np.full(self.dimension, lower, dtype=float) if np.isscalar(lower) else np.asarray(lower, dtype=float)
|
|
33
|
+
self.upper = np.full(self.dimension, upper, dtype=float) if np.isscalar(upper) else np.asarray(upper, dtype=float)
|
|
34
|
+
if self.lower.shape != (self.dimension,) or self.upper.shape != (self.dimension,):
|
|
35
|
+
raise ValueError("lower/upper must be scalars or arrays of shape (dimension,)")
|
|
36
|
+
if np.any(self.lower >= self.upper):
|
|
37
|
+
raise ValueError("each lower bound must be strictly less than its upper bound")
|
|
38
|
+
|
|
39
|
+
def evaluate(self, x):
|
|
40
|
+
"""Evaluate a solution vector and return its fitness as ``float``."""
|
|
41
|
+
x = np.asarray(x, dtype=float)
|
|
42
|
+
if x.shape != (self.dimension,):
|
|
43
|
+
raise ValueError(f"expected solution of shape ({self.dimension},), got {x.shape}")
|
|
44
|
+
return float(self._evaluate(x))
|
|
45
|
+
|
|
46
|
+
def _evaluate(self, x):
|
|
47
|
+
"""Compute the objective value for solution ``x``. Must be overridden."""
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def name(self):
|
|
52
|
+
return type(self).__name__
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Task: wraps a Problem with a stopping condition and bookkeeping."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OptimizationType(Enum):
|
|
9
|
+
MINIMIZATION = 1
|
|
10
|
+
MAXIMIZATION = -1
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Task:
|
|
14
|
+
"""An optimization run: a problem plus a budget and progress tracking.
|
|
15
|
+
|
|
16
|
+
The task counts evaluations, tracks the best solution found so far,
|
|
17
|
+
and records the convergence history. Algorithms should call
|
|
18
|
+
:meth:`eval` for every candidate solution and check :meth:`stopping_condition`.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
problem: The :class:`~ikn_library.problems.Problem` to optimize.
|
|
22
|
+
max_evals: Stop after this many fitness evaluations (optional).
|
|
23
|
+
max_iters: Stop after this many iterations (optional). The
|
|
24
|
+
algorithm must call :meth:`next_iter` once per iteration.
|
|
25
|
+
optimization_type: Minimize (default) or maximize.
|
|
26
|
+
|
|
27
|
+
At least one of ``max_evals`` / ``max_iters`` must be given.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, problem, max_evals=None, max_iters=None,
|
|
31
|
+
optimization_type=OptimizationType.MINIMIZATION):
|
|
32
|
+
if max_evals is None and max_iters is None:
|
|
33
|
+
raise ValueError("provide max_evals and/or max_iters")
|
|
34
|
+
self.problem = problem
|
|
35
|
+
self.max_evals = np.inf if max_evals is None else int(max_evals)
|
|
36
|
+
self.max_iters = np.inf if max_iters is None else int(max_iters)
|
|
37
|
+
self.optimization_type = optimization_type
|
|
38
|
+
|
|
39
|
+
self.evals = 0
|
|
40
|
+
self.iters = 0
|
|
41
|
+
self.best_x = None
|
|
42
|
+
self.best_fitness = np.inf
|
|
43
|
+
self.convergence = [] # best internal fitness after each iteration
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def dimension(self):
|
|
47
|
+
return self.problem.dimension
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def lower(self):
|
|
51
|
+
return self.problem.lower
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def upper(self):
|
|
55
|
+
return self.problem.upper
|
|
56
|
+
|
|
57
|
+
def repair(self, x):
|
|
58
|
+
"""Clip a solution back into the search-space bounds."""
|
|
59
|
+
return np.clip(x, self.lower, self.upper)
|
|
60
|
+
|
|
61
|
+
def eval(self, x):
|
|
62
|
+
"""Evaluate ``x``, update counters and the best-so-far solution.
|
|
63
|
+
|
|
64
|
+
Returns the fitness in *internal* form (maximization problems are
|
|
65
|
+
negated so that algorithms can always minimize).
|
|
66
|
+
"""
|
|
67
|
+
if self.stopping_condition():
|
|
68
|
+
return np.inf
|
|
69
|
+
fitness = self.problem.evaluate(x) * self.optimization_type.value
|
|
70
|
+
self.evals += 1
|
|
71
|
+
if fitness < self.best_fitness:
|
|
72
|
+
self.best_fitness = fitness
|
|
73
|
+
self.best_x = np.array(x, dtype=float)
|
|
74
|
+
return fitness
|
|
75
|
+
|
|
76
|
+
def next_iter(self):
|
|
77
|
+
"""Advance the iteration counter and record convergence."""
|
|
78
|
+
self.iters += 1
|
|
79
|
+
self.convergence.append(self.best_fitness)
|
|
80
|
+
|
|
81
|
+
def stopping_condition(self):
|
|
82
|
+
"""True when the evaluation or iteration budget is exhausted."""
|
|
83
|
+
return self.evals >= self.max_evals or self.iters >= self.max_iters
|
|
84
|
+
|
|
85
|
+
def result(self):
|
|
86
|
+
"""Return ``(best_x, best_fitness)`` in the problem's original sense."""
|
|
87
|
+
return self.best_x, self.best_fitness * self.optimization_type.value
|
|
88
|
+
|
|
89
|
+
def convergence_data(self):
|
|
90
|
+
"""Convergence history as ``(iterations, best_fitness_values)``."""
|
|
91
|
+
values = np.array(self.convergence) * self.optimization_type.value
|
|
92
|
+
return np.arange(1, len(values) + 1), values
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from ikn_library import Task
|
|
5
|
+
from ikn_library.algorithms import AntColonyOptimization
|
|
6
|
+
from ikn_library.problems import Rastrigin, Sphere
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_aco_converges_on_sphere():
|
|
10
|
+
task = Task(problem=Sphere(dimension=5), max_evals=10000)
|
|
11
|
+
algo = AntColonyOptimization(population_size=30, archive_size=50, seed=42)
|
|
12
|
+
best_x, best_fitness = algo.run(task)
|
|
13
|
+
assert best_fitness < 1e-3
|
|
14
|
+
assert best_x.shape == (5,)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_aco_improves_on_rastrigin():
|
|
18
|
+
problem = Rastrigin(dimension=5)
|
|
19
|
+
task = Task(problem=problem, max_evals=10000)
|
|
20
|
+
algo = AntColonyOptimization(population_size=30, archive_size=50, seed=1)
|
|
21
|
+
_, best_fitness = algo.run(task)
|
|
22
|
+
random_baseline = min(
|
|
23
|
+
problem.evaluate(x)
|
|
24
|
+
for x in np.random.default_rng(1).uniform(-5.12, 5.12, (100, 5))
|
|
25
|
+
)
|
|
26
|
+
assert best_fitness < random_baseline
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_aco_respects_bounds():
|
|
30
|
+
task = Task(problem=Sphere(dimension=3), max_evals=2000)
|
|
31
|
+
algo = AntColonyOptimization(population_size=10, archive_size=20, seed=7)
|
|
32
|
+
best_x, _ = algo.run(task)
|
|
33
|
+
assert np.all(best_x >= task.lower) and np.all(best_x <= task.upper)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_aco_is_reproducible_with_seed():
|
|
37
|
+
results = []
|
|
38
|
+
for _ in range(2):
|
|
39
|
+
task = Task(problem=Sphere(dimension=4), max_evals=3000)
|
|
40
|
+
algo = AntColonyOptimization(population_size=20, seed=123)
|
|
41
|
+
results.append(algo.run(task))
|
|
42
|
+
np.testing.assert_allclose(results[0][0], results[1][0])
|
|
43
|
+
assert results[0][1] == results[1][1]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_aco_respects_eval_budget():
|
|
47
|
+
task = Task(problem=Sphere(dimension=3), max_evals=500)
|
|
48
|
+
AntColonyOptimization(population_size=10, seed=0).run(task)
|
|
49
|
+
assert task.evals <= 500
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_invalid_archive_size():
|
|
53
|
+
with pytest.raises(ValueError):
|
|
54
|
+
AntColonyOptimization(archive_size=1)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from ikn_library import Task
|
|
5
|
+
from ikn_library.algorithms import BinaryAntColonyOptimization
|
|
6
|
+
from ikn_library.problems import Problem
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SubsetMatch(Problem):
|
|
10
|
+
"""Minimize the Hamming distance to a known target bit mask."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, target):
|
|
13
|
+
self.target = np.asarray(target, dtype=float)
|
|
14
|
+
super().__init__(dimension=len(self.target), lower=0.0, upper=1.0)
|
|
15
|
+
|
|
16
|
+
def _evaluate(self, x):
|
|
17
|
+
return float(np.sum((x > 0.5) != (self.target > 0.5)))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_binary_aco_finds_target_subset():
|
|
21
|
+
target = np.array([1, 0, 1, 1, 0, 0, 0, 1, 0, 0])
|
|
22
|
+
task = Task(problem=SubsetMatch(target), max_evals=6000)
|
|
23
|
+
algo = BinaryAntColonyOptimization(population_size=20, seed=42)
|
|
24
|
+
best_x, best_fitness = algo.run(task)
|
|
25
|
+
assert best_fitness == 0.0
|
|
26
|
+
np.testing.assert_array_equal(best_x, target)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_binary_aco_emits_only_bits():
|
|
30
|
+
target = np.ones(6)
|
|
31
|
+
task = Task(problem=SubsetMatch(target), max_evals=500)
|
|
32
|
+
best_x, _ = BinaryAntColonyOptimization(population_size=10, seed=3).run(task)
|
|
33
|
+
assert set(np.unique(best_x)) <= {0.0, 1.0}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_binary_aco_never_selects_empty_subset():
|
|
37
|
+
class CountOnes(Problem):
|
|
38
|
+
def __init__(self):
|
|
39
|
+
super().__init__(dimension=8, lower=0.0, upper=1.0)
|
|
40
|
+
|
|
41
|
+
def _evaluate(self, x):
|
|
42
|
+
assert np.sum(x) >= 1, "empty subset was evaluated"
|
|
43
|
+
return float(np.sum(x))
|
|
44
|
+
|
|
45
|
+
task = Task(problem=CountOnes(), max_evals=2000)
|
|
46
|
+
best_x, _ = BinaryAntColonyOptimization(population_size=15, seed=5).run(task)
|
|
47
|
+
assert np.sum(best_x) >= 1
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_binary_aco_is_reproducible_with_seed():
|
|
51
|
+
target = np.array([1, 0, 1, 0, 1, 0])
|
|
52
|
+
results = []
|
|
53
|
+
for _ in range(2):
|
|
54
|
+
task = Task(problem=SubsetMatch(target), max_evals=1000)
|
|
55
|
+
algo = BinaryAntColonyOptimization(population_size=10, seed=99)
|
|
56
|
+
results.append(algo.run(task))
|
|
57
|
+
np.testing.assert_array_equal(results[0][0], results[1][0])
|
|
58
|
+
assert results[0][1] == results[1][1]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_binary_aco_respects_eval_budget():
|
|
62
|
+
task = Task(problem=SubsetMatch(np.ones(5)), max_evals=300)
|
|
63
|
+
BinaryAntColonyOptimization(population_size=10, seed=0).run(task)
|
|
64
|
+
assert task.evals <= 300
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.mark.parametrize("kwargs", [
|
|
68
|
+
{"evaporation": 0.0},
|
|
69
|
+
{"evaporation": 1.0},
|
|
70
|
+
{"tau_min": 0.5, "tau_max": 0.4},
|
|
71
|
+
])
|
|
72
|
+
def test_binary_aco_invalid_params(kwargs):
|
|
73
|
+
with pytest.raises(ValueError):
|
|
74
|
+
BinaryAntColonyOptimization(**kwargs)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
sklearn = pytest.importorskip("sklearn")
|
|
5
|
+
|
|
6
|
+
from sklearn.datasets import make_classification
|
|
7
|
+
|
|
8
|
+
from ikn_library import Task
|
|
9
|
+
from ikn_library.algorithms import BinaryAntColonyOptimization
|
|
10
|
+
from ikn_library.problems import FeatureSelectionProblem
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture(scope="module")
|
|
14
|
+
def dataset():
|
|
15
|
+
return make_classification(
|
|
16
|
+
n_samples=120, n_features=10, n_informative=3, n_redundant=0,
|
|
17
|
+
n_repeated=0, shuffle=False, random_state=42,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_selected_features_and_mask(dataset):
|
|
22
|
+
X, y = dataset
|
|
23
|
+
problem = FeatureSelectionProblem(X, y)
|
|
24
|
+
x = np.array([0.9, 0.1, 0.6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7])
|
|
25
|
+
np.testing.assert_array_equal(problem.selected_features(x), [0, 2, 9])
|
|
26
|
+
assert problem.feature_mask(x).sum() == 3
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_empty_subset_gets_worst_fitness(dataset):
|
|
30
|
+
X, y = dataset
|
|
31
|
+
problem = FeatureSelectionProblem(X, y)
|
|
32
|
+
assert problem.evaluate(np.zeros(10)) == 1.0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_fitness_in_unit_interval(dataset):
|
|
36
|
+
X, y = dataset
|
|
37
|
+
problem = FeatureSelectionProblem(X, y, cv=3)
|
|
38
|
+
fitness = problem.evaluate(np.ones(10))
|
|
39
|
+
assert 0.0 <= fitness <= 1.0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_binary_aco_feature_selection_improves(dataset):
|
|
43
|
+
X, y = dataset
|
|
44
|
+
problem = FeatureSelectionProblem(X, y, cv=3)
|
|
45
|
+
all_features_fitness = problem.evaluate(np.ones(10))
|
|
46
|
+
|
|
47
|
+
task = Task(problem=problem, max_evals=300)
|
|
48
|
+
algo = BinaryAntColonyOptimization(population_size=10, seed=42)
|
|
49
|
+
best_x, best_fitness = algo.run(task)
|
|
50
|
+
|
|
51
|
+
assert best_fitness <= all_features_fitness
|
|
52
|
+
assert len(problem.selected_features(best_x)) >= 1
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_input_validation(dataset):
|
|
56
|
+
X, y = dataset
|
|
57
|
+
with pytest.raises(ValueError):
|
|
58
|
+
FeatureSelectionProblem(X, y[:-5])
|
|
59
|
+
with pytest.raises(ValueError):
|
|
60
|
+
FeatureSelectionProblem(X, y, alpha=1.5)
|
|
61
|
+
with pytest.raises(ValueError):
|
|
62
|
+
FeatureSelectionProblem(X[0], y)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from ikn_library.problems import Ackley, Problem, Rastrigin, Sphere
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.parametrize("cls", [Sphere, Rastrigin, Ackley])
|
|
8
|
+
def test_benchmark_optimum_is_zero(cls):
|
|
9
|
+
problem = cls(dimension=10)
|
|
10
|
+
assert problem.evaluate(np.zeros(10)) == pytest.approx(0.0, abs=1e-9)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_evaluate_rejects_wrong_shape():
|
|
14
|
+
problem = Sphere(dimension=5)
|
|
15
|
+
with pytest.raises(ValueError):
|
|
16
|
+
problem.evaluate(np.zeros(3))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_custom_problem():
|
|
20
|
+
class Linear(Problem):
|
|
21
|
+
def __init__(self):
|
|
22
|
+
super().__init__(dimension=3, lower=0.0, upper=1.0)
|
|
23
|
+
|
|
24
|
+
def _evaluate(self, x):
|
|
25
|
+
return np.sum(x)
|
|
26
|
+
|
|
27
|
+
assert Linear().evaluate([0.1, 0.2, 0.3]) == pytest.approx(0.6)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_invalid_bounds_raise():
|
|
31
|
+
with pytest.raises(ValueError):
|
|
32
|
+
Sphere(dimension=2, lower=1.0, upper=-1.0)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from ikn_library import OptimizationType, Task
|
|
5
|
+
from ikn_library.problems import Sphere
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_requires_a_budget():
|
|
9
|
+
with pytest.raises(ValueError):
|
|
10
|
+
Task(problem=Sphere(dimension=2))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_counts_evals_and_tracks_best():
|
|
14
|
+
task = Task(problem=Sphere(dimension=2), max_evals=10)
|
|
15
|
+
task.eval(np.array([1.0, 1.0]))
|
|
16
|
+
task.eval(np.array([0.5, 0.5]))
|
|
17
|
+
assert task.evals == 2
|
|
18
|
+
assert task.best_fitness == pytest.approx(0.5)
|
|
19
|
+
np.testing.assert_allclose(task.best_x, [0.5, 0.5])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_stops_at_max_evals():
|
|
23
|
+
task = Task(problem=Sphere(dimension=2), max_evals=3)
|
|
24
|
+
for _ in range(5):
|
|
25
|
+
task.eval(np.array([1.0, 1.0]))
|
|
26
|
+
assert task.evals == 3
|
|
27
|
+
assert task.stopping_condition()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_repair_clips_to_bounds():
|
|
31
|
+
task = Task(problem=Sphere(dimension=2), max_evals=10)
|
|
32
|
+
repaired = task.repair(np.array([100.0, -100.0]))
|
|
33
|
+
np.testing.assert_allclose(repaired, [5.12, -5.12])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_maximization_result_sign():
|
|
37
|
+
task = Task(
|
|
38
|
+
problem=Sphere(dimension=2),
|
|
39
|
+
max_evals=10,
|
|
40
|
+
optimization_type=OptimizationType.MAXIMIZATION,
|
|
41
|
+
)
|
|
42
|
+
task.eval(np.array([2.0, 0.0]))
|
|
43
|
+
_, best = task.result()
|
|
44
|
+
assert best == pytest.approx(4.0)
|