abcopt 0.2.0__py3-none-any.whl
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.
- abcopt/__init__.py +7 -0
- abcopt/core.py +182 -0
- abcopt/sklearn_wrapper.py +211 -0
- abcopt-0.2.0.dist-info/METADATA +264 -0
- abcopt-0.2.0.dist-info/RECORD +8 -0
- abcopt-0.2.0.dist-info/WHEEL +5 -0
- abcopt-0.2.0.dist-info/licenses/LICENSE +21 -0
- abcopt-0.2.0.dist-info/top_level.txt +1 -0
abcopt/__init__.py
ADDED
abcopt/core.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CanonicalABCSolver:
|
|
5
|
+
"""
|
|
6
|
+
Canonical Artificial Bee Colony (ABC) algorithm.
|
|
7
|
+
|
|
8
|
+
A faithful implementation of the algorithm proposed by D. Karaboğa (2005):
|
|
9
|
+
employed bees, onlooker bees (roulette-wheel selection) and scout bees,
|
|
10
|
+
with a single dimension perturbed per move.
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
objective_func : callable
|
|
15
|
+
Function taking a 1-D ``np.ndarray`` of length ``n_params`` and
|
|
16
|
+
returning a scalar cost to **minimize**.
|
|
17
|
+
n_params : int
|
|
18
|
+
Number of dimensions (decision variables).
|
|
19
|
+
lb, ub : array-like of shape (n_params,)
|
|
20
|
+
Lower and upper bounds for each dimension.
|
|
21
|
+
pop_size : int, default=20
|
|
22
|
+
Number of food sources (SN). Must be at least 2.
|
|
23
|
+
max_evals : int, default=1000
|
|
24
|
+
Total budget of objective-function evaluations.
|
|
25
|
+
limit : int or None, default=None
|
|
26
|
+
Number of unsuccessful trials before a food source is abandoned.
|
|
27
|
+
Defaults to ``pop_size * n_params``.
|
|
28
|
+
random_state : int, np.random.Generator or None, default=None
|
|
29
|
+
Seed or generator for reproducible runs.
|
|
30
|
+
|
|
31
|
+
Attributes
|
|
32
|
+
----------
|
|
33
|
+
best_params : np.ndarray
|
|
34
|
+
Best solution found.
|
|
35
|
+
best_cost : float
|
|
36
|
+
Objective value of ``best_params``.
|
|
37
|
+
history : list of (int, float)
|
|
38
|
+
``(evaluations_used, best_cost_so_far)`` after each cycle.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, objective_func, n_params, lb, ub, pop_size=20,
|
|
42
|
+
max_evals=1000, limit=None, random_state=None):
|
|
43
|
+
self.objective_func = objective_func
|
|
44
|
+
self.D = int(n_params)
|
|
45
|
+
self.lb = np.asarray(lb, dtype=float).ravel()
|
|
46
|
+
self.ub = np.asarray(ub, dtype=float).ravel()
|
|
47
|
+
self.SN = int(pop_size)
|
|
48
|
+
self.total_evaluation = int(max_evals)
|
|
49
|
+
self.limit = limit if limit is not None else self.SN * self.D
|
|
50
|
+
self.random_state = random_state
|
|
51
|
+
|
|
52
|
+
self._validate()
|
|
53
|
+
|
|
54
|
+
self.best_params = None
|
|
55
|
+
self.best_cost = float("inf")
|
|
56
|
+
self.history = []
|
|
57
|
+
|
|
58
|
+
def _validate(self):
|
|
59
|
+
if not callable(self.objective_func):
|
|
60
|
+
raise TypeError("objective_func must be callable.")
|
|
61
|
+
if self.D < 1:
|
|
62
|
+
raise ValueError("n_params must be at least 1.")
|
|
63
|
+
if self.lb.shape != (self.D,) or self.ub.shape != (self.D,):
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"lb and ub must both have length n_params={self.D}, "
|
|
66
|
+
f"got {self.lb.size} and {self.ub.size}."
|
|
67
|
+
)
|
|
68
|
+
if np.any(self.lb > self.ub):
|
|
69
|
+
raise ValueError("Every lower bound must be <= its upper bound.")
|
|
70
|
+
if self.SN < 2:
|
|
71
|
+
raise ValueError("pop_size must be at least 2 (neighbours are needed).")
|
|
72
|
+
if self.total_evaluation < self.SN:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"max_evals ({self.total_evaluation}) must be >= pop_size ({self.SN}) "
|
|
75
|
+
"so that the initial population can be evaluated."
|
|
76
|
+
)
|
|
77
|
+
if self.limit < 1:
|
|
78
|
+
raise ValueError("limit must be at least 1.")
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _calculate_fitness(cost):
|
|
82
|
+
"""Convert a cost (to be minimized) into a fitness (to be maximized)."""
|
|
83
|
+
return 1.0 / (1.0 + cost) if cost >= 0 else 1.0 + abs(cost)
|
|
84
|
+
|
|
85
|
+
def _random_solution(self):
|
|
86
|
+
return self.lb + self._rng.random(self.D) * (self.ub - self.lb)
|
|
87
|
+
|
|
88
|
+
def _neighbour(self, i):
|
|
89
|
+
k = self._rng.integers(0, self.SN - 1)
|
|
90
|
+
return k if k < i else k + 1 # uniform over all indices except i
|
|
91
|
+
|
|
92
|
+
def _perturb(self, current, neighbour):
|
|
93
|
+
"""Canonical move: change exactly one randomly chosen dimension."""
|
|
94
|
+
j = self._rng.integers(0, self.D)
|
|
95
|
+
phi = self._rng.uniform(-1.0, 1.0)
|
|
96
|
+
new_sol = np.copy(current)
|
|
97
|
+
new_sol[j] = current[j] + phi * (current[j] - neighbour[j])
|
|
98
|
+
new_sol[j] = np.clip(new_sol[j], self.lb[j], self.ub[j])
|
|
99
|
+
return new_sol
|
|
100
|
+
|
|
101
|
+
def _evaluate(self, x):
|
|
102
|
+
cost = float(self.objective_func(x))
|
|
103
|
+
if np.isnan(cost):
|
|
104
|
+
cost = float("inf")
|
|
105
|
+
self._evals += 1
|
|
106
|
+
return cost
|
|
107
|
+
|
|
108
|
+
def _try_improve(self, i):
|
|
109
|
+
"""Greedy selection between food source i and a mutated copy."""
|
|
110
|
+
k = self._neighbour(i)
|
|
111
|
+
new_sol = self._perturb(self.foods[i], self.foods[k])
|
|
112
|
+
new_cost = self._evaluate(new_sol)
|
|
113
|
+
new_fit = self._calculate_fitness(new_cost)
|
|
114
|
+
if new_fit > self.fitness[i]:
|
|
115
|
+
self.foods[i] = new_sol
|
|
116
|
+
self.costs[i] = new_cost
|
|
117
|
+
self.fitness[i] = new_fit
|
|
118
|
+
self.trial[i] = 0
|
|
119
|
+
else:
|
|
120
|
+
self.trial[i] += 1
|
|
121
|
+
|
|
122
|
+
def _memorize_best(self):
|
|
123
|
+
idx = int(np.argmin(self.costs))
|
|
124
|
+
if self.costs[idx] < self.best_cost:
|
|
125
|
+
self.best_cost = float(self.costs[idx])
|
|
126
|
+
self.best_params = np.copy(self.foods[idx])
|
|
127
|
+
|
|
128
|
+
def solve(self):
|
|
129
|
+
"""
|
|
130
|
+
Run the optimization.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
best_params : np.ndarray
|
|
135
|
+
best_cost : float
|
|
136
|
+
history : list of (int, float)
|
|
137
|
+
"""
|
|
138
|
+
self._rng = np.random.default_rng(self.random_state)
|
|
139
|
+
self._evals = 0
|
|
140
|
+
self.best_params = None
|
|
141
|
+
self.best_cost = float("inf")
|
|
142
|
+
self.history = []
|
|
143
|
+
|
|
144
|
+
# Initialization
|
|
145
|
+
self.foods = self.lb + self._rng.random((self.SN, self.D)) * (self.ub - self.lb)
|
|
146
|
+
self.costs = np.array([self._evaluate(x) for x in self.foods])
|
|
147
|
+
self.fitness = np.array([self._calculate_fitness(c) for c in self.costs])
|
|
148
|
+
self.trial = np.zeros(self.SN, dtype=int)
|
|
149
|
+
self._memorize_best()
|
|
150
|
+
self.history.append((self._evals, self.best_cost))
|
|
151
|
+
|
|
152
|
+
while self._evals < self.total_evaluation:
|
|
153
|
+
# 1. Employed bees: every food source is visited once.
|
|
154
|
+
for i in range(self.SN):
|
|
155
|
+
if self._evals >= self.total_evaluation:
|
|
156
|
+
break
|
|
157
|
+
self._try_improve(i)
|
|
158
|
+
|
|
159
|
+
# 2. Onlooker bees: roulette-wheel selection proportional to fitness.
|
|
160
|
+
total_fit = np.sum(self.fitness)
|
|
161
|
+
prob = (self.fitness / total_fit) if total_fit > 0 else np.full(self.SN, 1.0 / self.SN)
|
|
162
|
+
t, i = 0, 0
|
|
163
|
+
while t < self.SN and self._evals < self.total_evaluation:
|
|
164
|
+
if self._rng.random() < prob[i]:
|
|
165
|
+
t += 1
|
|
166
|
+
self._try_improve(i)
|
|
167
|
+
i = (i + 1) % self.SN
|
|
168
|
+
|
|
169
|
+
self._memorize_best()
|
|
170
|
+
|
|
171
|
+
# 3. Scout bee: abandon the most exhausted source if over the limit.
|
|
172
|
+
worst = int(np.argmax(self.trial))
|
|
173
|
+
if self.trial[worst] > self.limit and self._evals < self.total_evaluation:
|
|
174
|
+
self.foods[worst] = self._random_solution()
|
|
175
|
+
self.costs[worst] = self._evaluate(self.foods[worst])
|
|
176
|
+
self.fitness[worst] = self._calculate_fitness(self.costs[worst])
|
|
177
|
+
self.trial[worst] = 0
|
|
178
|
+
self._memorize_best()
|
|
179
|
+
|
|
180
|
+
self.history.append((self._evals, self.best_cost))
|
|
181
|
+
|
|
182
|
+
return self.best_params, self.best_cost, self.history
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import warnings
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from sklearn.base import BaseEstimator, MetaEstimatorMixin, clone
|
|
6
|
+
from sklearn.model_selection import cross_val_score
|
|
7
|
+
|
|
8
|
+
from .core import CanonicalABCSolver
|
|
9
|
+
|
|
10
|
+
_VALID_TYPES = ("float", "int", "categorical")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ABCSearchCV(MetaEstimatorMixin, BaseEstimator):
|
|
14
|
+
"""
|
|
15
|
+
Hyperparameter search with the canonical Artificial Bee Colony algorithm.
|
|
16
|
+
|
|
17
|
+
Works like ``GridSearchCV`` / ``RandomizedSearchCV``: every candidate is
|
|
18
|
+
scored with cross-validation and the best one is refit on the full data.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
estimator : estimator object
|
|
23
|
+
Any scikit-learn compatible estimator.
|
|
24
|
+
param_space : dict
|
|
25
|
+
Maps each hyperparameter name to a specification:
|
|
26
|
+
|
|
27
|
+
- ``{"type": "float", "range": (low, high)}``
|
|
28
|
+
- ``{"type": "float", "range": (low, high), "log": True}``
|
|
29
|
+
- ``{"type": "int", "range": (low, high)}`` (both ends inclusive)
|
|
30
|
+
- ``{"type": "categorical", "choices": [a, b, c]}``
|
|
31
|
+
cv : int or cross-validation generator, default=3
|
|
32
|
+
scoring : str or callable, default="accuracy"
|
|
33
|
+
Any scikit-learn scorer. Higher is better (use ``neg_*`` scorers
|
|
34
|
+
for errors), exactly as in ``GridSearchCV``.
|
|
35
|
+
pop_size : int, default=20
|
|
36
|
+
Number of food sources.
|
|
37
|
+
max_evals : int, default=200
|
|
38
|
+
Total number of candidates evaluated (each one is a full CV run).
|
|
39
|
+
limit : int or None, default=None
|
|
40
|
+
Abandonment limit for scout bees.
|
|
41
|
+
random_state : int or None, default=None
|
|
42
|
+
Seed for reproducible searches.
|
|
43
|
+
n_jobs : int or None, default=None
|
|
44
|
+
Parallel jobs used inside each cross-validation.
|
|
45
|
+
error_score : "raise" or float, default=np.nan
|
|
46
|
+
What to do when a candidate fails to fit. ``"raise"`` re-raises the
|
|
47
|
+
error; otherwise the candidate gets this score (NaN is treated as the
|
|
48
|
+
worst possible score) and a warning is issued.
|
|
49
|
+
refit : bool, default=True
|
|
50
|
+
Refit the best estimator on the whole dataset.
|
|
51
|
+
verbose : bool, default=False
|
|
52
|
+
|
|
53
|
+
Attributes
|
|
54
|
+
----------
|
|
55
|
+
best_params_ : dict
|
|
56
|
+
best_score_ : float
|
|
57
|
+
Mean cross-validated score of the best candidate.
|
|
58
|
+
best_estimator_ : estimator
|
|
59
|
+
Only available when ``refit=True``.
|
|
60
|
+
history_ : list of (int, float)
|
|
61
|
+
``(evaluations_used, best_score_so_far)`` after each ABC cycle.
|
|
62
|
+
results_ : list of dict
|
|
63
|
+
Every evaluated candidate with its ``params`` and ``score``.
|
|
64
|
+
n_evals_ : int
|
|
65
|
+
Number of candidates evaluated.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, estimator, param_space, cv=3, scoring="accuracy",
|
|
69
|
+
pop_size=20, max_evals=200, limit=None, random_state=None,
|
|
70
|
+
n_jobs=None, error_score=np.nan, refit=True, verbose=False):
|
|
71
|
+
self.estimator = estimator
|
|
72
|
+
self.param_space = param_space
|
|
73
|
+
self.cv = cv
|
|
74
|
+
self.scoring = scoring
|
|
75
|
+
self.pop_size = pop_size
|
|
76
|
+
self.max_evals = max_evals
|
|
77
|
+
self.limit = limit
|
|
78
|
+
self.random_state = random_state
|
|
79
|
+
self.n_jobs = n_jobs
|
|
80
|
+
self.error_score = error_score
|
|
81
|
+
self.refit = refit
|
|
82
|
+
self.verbose = verbose
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------------------------------ #
|
|
85
|
+
# Search-space encoding: every parameter becomes one continuous
|
|
86
|
+
# dimension that ABC can search; _decode maps it back.
|
|
87
|
+
# ------------------------------------------------------------------ #
|
|
88
|
+
def _build_space(self):
|
|
89
|
+
if not isinstance(self.param_space, dict) or not self.param_space:
|
|
90
|
+
raise ValueError("param_space must be a non-empty dict.")
|
|
91
|
+
|
|
92
|
+
keys, lb, ub = [], [], []
|
|
93
|
+
for name, spec in self.param_space.items():
|
|
94
|
+
p_type = spec.get("type")
|
|
95
|
+
if p_type not in _VALID_TYPES:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Parameter '{name}': type must be one of {_VALID_TYPES}, got {p_type!r}."
|
|
98
|
+
)
|
|
99
|
+
if p_type == "categorical":
|
|
100
|
+
choices = spec.get("choices")
|
|
101
|
+
if not choices:
|
|
102
|
+
raise ValueError(f"Parameter '{name}': 'choices' must be a non-empty list.")
|
|
103
|
+
lo, hi = 0.0, float(len(choices))
|
|
104
|
+
else:
|
|
105
|
+
low, high = spec.get("range", (None, None))
|
|
106
|
+
if low is None or high is None or low > high:
|
|
107
|
+
raise ValueError(f"Parameter '{name}': 'range' must be (low, high) with low <= high.")
|
|
108
|
+
if spec.get("log"):
|
|
109
|
+
if low <= 0:
|
|
110
|
+
raise ValueError(f"Parameter '{name}': log scale needs a positive range.")
|
|
111
|
+
lo, hi = math.log10(low), math.log10(high)
|
|
112
|
+
elif p_type == "int":
|
|
113
|
+
# widen by 0.5 so both ends are reachable after rounding
|
|
114
|
+
lo, hi = low - 0.5, high + 0.5
|
|
115
|
+
else:
|
|
116
|
+
lo, hi = float(low), float(high)
|
|
117
|
+
keys.append(name)
|
|
118
|
+
lb.append(lo)
|
|
119
|
+
ub.append(hi)
|
|
120
|
+
return keys, lb, ub
|
|
121
|
+
|
|
122
|
+
def _decode(self, vector):
|
|
123
|
+
params = {}
|
|
124
|
+
for name, val in zip(self._keys, vector):
|
|
125
|
+
spec = self.param_space[name]
|
|
126
|
+
p_type = spec["type"]
|
|
127
|
+
if p_type == "categorical":
|
|
128
|
+
choices = spec["choices"]
|
|
129
|
+
params[name] = choices[min(int(val), len(choices) - 1)]
|
|
130
|
+
continue
|
|
131
|
+
if spec.get("log"):
|
|
132
|
+
val = 10 ** val
|
|
133
|
+
if p_type == "int":
|
|
134
|
+
low, high = spec["range"]
|
|
135
|
+
params[name] = int(min(max(round(val), low), high))
|
|
136
|
+
else:
|
|
137
|
+
params[name] = float(val)
|
|
138
|
+
return params
|
|
139
|
+
|
|
140
|
+
# ------------------------------------------------------------------ #
|
|
141
|
+
def fit(self, X, y=None, groups=None):
|
|
142
|
+
self._keys, lb, ub = self._build_space()
|
|
143
|
+
self.results_ = []
|
|
144
|
+
|
|
145
|
+
def objective(vector):
|
|
146
|
+
params = self._decode(vector)
|
|
147
|
+
model = clone(self.estimator).set_params(**params)
|
|
148
|
+
try:
|
|
149
|
+
scores = cross_val_score(
|
|
150
|
+
model, X, y, groups=groups, cv=self.cv, scoring=self.scoring,
|
|
151
|
+
n_jobs=self.n_jobs, error_score="raise",
|
|
152
|
+
)
|
|
153
|
+
score = float(np.mean(scores))
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
if self.error_score == "raise":
|
|
156
|
+
raise
|
|
157
|
+
warnings.warn(
|
|
158
|
+
f"Candidate {params} failed and was given score "
|
|
159
|
+
f"{self.error_score}: {type(exc).__name__}: {exc}",
|
|
160
|
+
RuntimeWarning,
|
|
161
|
+
stacklevel=2,
|
|
162
|
+
)
|
|
163
|
+
score = float(self.error_score)
|
|
164
|
+
self.results_.append({"params": params, "score": score})
|
|
165
|
+
if self.verbose:
|
|
166
|
+
print(f"[{len(self.results_)}/{self.max_evals}] score={score:.5f} {params}")
|
|
167
|
+
# ABC minimizes; NaN scores become +inf cost inside the solver.
|
|
168
|
+
return -score
|
|
169
|
+
|
|
170
|
+
solver = CanonicalABCSolver(
|
|
171
|
+
objective, len(self._keys), lb, ub,
|
|
172
|
+
pop_size=self.pop_size, max_evals=self.max_evals,
|
|
173
|
+
limit=self.limit, random_state=self.random_state,
|
|
174
|
+
)
|
|
175
|
+
best_vec, best_cost, history = solver.solve()
|
|
176
|
+
|
|
177
|
+
if not np.isfinite(best_cost):
|
|
178
|
+
raise RuntimeError(
|
|
179
|
+
"All candidates failed. Check param_space and the estimator; "
|
|
180
|
+
"set error_score='raise' to see the underlying error."
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
self.best_params_ = self._decode(best_vec)
|
|
184
|
+
self.best_score_ = -best_cost
|
|
185
|
+
self.history_ = [(n, -c) for n, c in history]
|
|
186
|
+
self.n_evals_ = len(self.results_)
|
|
187
|
+
|
|
188
|
+
if self.refit:
|
|
189
|
+
self.best_estimator_ = clone(self.estimator).set_params(**self.best_params_)
|
|
190
|
+
if y is None:
|
|
191
|
+
self.best_estimator_.fit(X)
|
|
192
|
+
else:
|
|
193
|
+
self.best_estimator_.fit(X, y)
|
|
194
|
+
return self
|
|
195
|
+
|
|
196
|
+
# ------------------------------------------------------------------ #
|
|
197
|
+
def _check_refit(self, method):
|
|
198
|
+
if not hasattr(self, "best_estimator_"):
|
|
199
|
+
raise AttributeError(
|
|
200
|
+
f"{method} is only available after fit() with refit=True."
|
|
201
|
+
)
|
|
202
|
+
return self.best_estimator_
|
|
203
|
+
|
|
204
|
+
def predict(self, X):
|
|
205
|
+
return self._check_refit("predict").predict(X)
|
|
206
|
+
|
|
207
|
+
def predict_proba(self, X):
|
|
208
|
+
return self._check_refit("predict_proba").predict_proba(X)
|
|
209
|
+
|
|
210
|
+
def score(self, X, y):
|
|
211
|
+
return self._check_refit("score").score(X, y)
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: abcopt
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Canonical Artificial Bee Colony (ABC) optimization with a scikit-learn compatible hyperparameter search
|
|
5
|
+
Author-email: Yusuf Korkmazyiğit <yusuf.korkmazyigit@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yusufkorkmazyigit/abc-optimizer-lib
|
|
8
|
+
Project-URL: Issues, https://github.com/yusufkorkmazyigit/abc-optimizer-lib/issues
|
|
9
|
+
Keywords: artificial bee colony,swarm intelligence,metaheuristics,hyperparameter optimization,scikit-learn
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: numpy>=1.21
|
|
18
|
+
Requires-Dist: scikit-learn>=1.0
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# 🐝 abcopt
|
|
24
|
+
|
|
25
|
+
**Canonical Artificial Bee Colony (ABC) optimization in pure Python, with a scikit-learn compatible hyperparameter search.**
|
|
26
|
+
|
|
27
|
+
[](https://pypi.org/project/abcopt/)
|
|
28
|
+
[](https://github.com/yusufkorkmazyigit/abc-optimizer-lib/actions/workflows/tests.yml)
|
|
29
|
+

|
|
30
|
+

|
|
31
|
+

|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
**abcopt** is a faithful implementation of the **Artificial Bee Colony** algorithm proposed by **Derviş Karaboğa (2005)** at Erciyes University. It is useful for two things:
|
|
35
|
+
|
|
36
|
+
1. **Black-box optimization:** minimize any objective function within box constraints, with no gradients required.
|
|
37
|
+
2. **Hyperparameter tuning:** a drop-in alternative to `GridSearchCV` / `RandomizedSearchCV` for any scikit-learn compatible estimator (scikit-learn, LightGBM, XGBoost, …).
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## ✨ Features
|
|
42
|
+
|
|
43
|
+
- **Canonical algorithm:** employed, onlooker (roulette-wheel selection) and scout bee phases, following the original paper; one dimension is perturbed per move.
|
|
44
|
+
- **scikit-learn API:** `ABCSearchCV` exposes `fit`, `predict`, `score`, `best_params_`, `best_score_` and `best_estimator_`.
|
|
45
|
+
- **Mixed search spaces:** `float` (optionally log-scaled), `int` and `categorical` hyperparameters.
|
|
46
|
+
- **Reproducible:** pass `random_state` and every run is identical.
|
|
47
|
+
- **Evaluation budget:** you set `max_evals`, so the run cost is predictable.
|
|
48
|
+
- **Robust:** failing candidates raise a warning instead of being hidden (or stop the search with `error_score="raise"`), and invalid inputs fail fast with clear messages.
|
|
49
|
+
- **Convergence history:** every run returns its best-so-far curve for plotting and comparison.
|
|
50
|
+
- **Tested:** 30 unit tests run on every push via GitHub Actions.
|
|
51
|
+
- **Lightweight:** depends only on `numpy` and `scikit-learn`.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 📦 Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install abcopt
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from abcopt import ABCSearchCV, CanonicalABCSolver
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
For development:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
git clone https://github.com/yusufkorkmazyigit/abc-optimizer-lib.git
|
|
69
|
+
cd abc-optimizer-lib
|
|
70
|
+
pip install -e ".[test]"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 🚀 Quick Start
|
|
76
|
+
|
|
77
|
+
### 1. Hyperparameter tuning with `ABCSearchCV`
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import lightgbm as lgb
|
|
81
|
+
from sklearn.datasets import load_breast_cancer
|
|
82
|
+
from abcopt import ABCSearchCV
|
|
83
|
+
|
|
84
|
+
X, y = load_breast_cancer(return_X_y=True)
|
|
85
|
+
|
|
86
|
+
param_space = {
|
|
87
|
+
"learning_rate": {"type": "float", "range": (0.01, 0.3), "log": True},
|
|
88
|
+
"n_estimators": {"type": "int", "range": (50, 500)},
|
|
89
|
+
"num_leaves": {"type": "int", "range": (20, 100)},
|
|
90
|
+
"boosting_type": {"type": "categorical", "choices": ["gbdt", "dart"]},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
search = ABCSearchCV(
|
|
94
|
+
estimator=lgb.LGBMClassifier(verbosity=-1),
|
|
95
|
+
param_space=param_space,
|
|
96
|
+
cv=3,
|
|
97
|
+
scoring="accuracy",
|
|
98
|
+
pop_size=20, # number of food sources
|
|
99
|
+
max_evals=100, # total objective evaluations (each one = one CV run)
|
|
100
|
+
random_state=42, # reproducible search
|
|
101
|
+
)
|
|
102
|
+
search.fit(X, y)
|
|
103
|
+
|
|
104
|
+
print("Best CV score:", search.best_score_)
|
|
105
|
+
print("Best params: ", search.best_params_)
|
|
106
|
+
y_pred = search.predict(X) # uses best_estimator_, refit on the full data
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 2. Function minimization with `CanonicalABCSolver`
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
import numpy as np
|
|
113
|
+
from abcopt import CanonicalABCSolver
|
|
114
|
+
|
|
115
|
+
def sphere(x):
|
|
116
|
+
return np.sum(x ** 2) # global minimum: 0 at x = 0
|
|
117
|
+
|
|
118
|
+
solver = CanonicalABCSolver(
|
|
119
|
+
objective_func=sphere,
|
|
120
|
+
n_params=3,
|
|
121
|
+
lb=[-10, -10, -10],
|
|
122
|
+
ub=[10, 10, 10],
|
|
123
|
+
max_evals=2000,
|
|
124
|
+
random_state=0,
|
|
125
|
+
)
|
|
126
|
+
best_x, best_cost, history = solver.solve()
|
|
127
|
+
print(f"x* = {best_x}, f(x*) = {best_cost:.5f}")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 3. Plotting convergence
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
import matplotlib.pyplot as plt
|
|
134
|
+
|
|
135
|
+
evals, costs = zip(*history) # solver: best cost so far
|
|
136
|
+
# evals, scores = zip(*search.history_) # ABCSearchCV: best CV score so far
|
|
137
|
+
plt.plot(evals, costs)
|
|
138
|
+
plt.xlabel("Function evaluations")
|
|
139
|
+
plt.ylabel("Best cost")
|
|
140
|
+
plt.title("ABC convergence")
|
|
141
|
+
plt.show()
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 📖 API Reference
|
|
147
|
+
|
|
148
|
+
### `CanonicalABCSolver`
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
CanonicalABCSolver(objective_func, n_params, lb, ub,
|
|
152
|
+
pop_size=20, max_evals=1000, limit=None, random_state=None)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
| Parameter | Description |
|
|
156
|
+
|---|---|
|
|
157
|
+
| `objective_func` | Callable taking a 1-D `np.ndarray` and returning a scalar cost to **minimize** |
|
|
158
|
+
| `n_params` | Number of dimensions |
|
|
159
|
+
| `lb`, `ub` | Lower / upper bounds per dimension |
|
|
160
|
+
| `pop_size` | Number of food sources (SN) |
|
|
161
|
+
| `max_evals` | Total objective-function evaluation budget |
|
|
162
|
+
| `limit` | Trials without improvement before a source is abandoned (default: `pop_size × n_params`) |
|
|
163
|
+
| `random_state` | Seed (or `np.random.Generator`) for reproducible runs |
|
|
164
|
+
|
|
165
|
+
`NaN` objective values are treated as the worst possible cost.
|
|
166
|
+
|
|
167
|
+
`solve()` returns `(best_params, best_cost, history)`, where `history` is a list of `(eval_count, best_cost)` tuples.
|
|
168
|
+
|
|
169
|
+
### `ABCSearchCV`
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
ABCSearchCV(estimator, param_space, cv=3, scoring="accuracy",
|
|
173
|
+
pop_size=20, max_evals=200, limit=None, random_state=None,
|
|
174
|
+
n_jobs=None, error_score=np.nan, refit=True, verbose=False)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**`param_space`**: one entry per hyperparameter:
|
|
178
|
+
|
|
179
|
+
| Spec | Example |
|
|
180
|
+
|---|---|
|
|
181
|
+
| Continuous | `{"type": "float", "range": (0.01, 0.3)}` |
|
|
182
|
+
| Continuous, log scale | `{"type": "float", "range": (1e-4, 1e-1), "log": True}` |
|
|
183
|
+
| Integer (both ends inclusive) | `{"type": "int", "range": (2, 12)}` |
|
|
184
|
+
| Categorical | `{"type": "categorical", "choices": ["gini", "entropy"]}` |
|
|
185
|
+
|
|
186
|
+
- **`scoring`**: any scikit-learn scorer; higher is better, exactly like `GridSearchCV` (use `neg_*` scorers for errors).
|
|
187
|
+
- **`error_score`**: score given to candidates that fail to fit (a `RuntimeWarning` is issued); `"raise"` stops the search instead.
|
|
188
|
+
- **`n_jobs`**: parallel jobs inside each cross-validation.
|
|
189
|
+
- **After `fit`:** `best_params_`, `best_score_`, `best_estimator_` (when `refit=True`), `history_`, `results_` (every candidate with its score), `n_evals_`.
|
|
190
|
+
- **Methods:** `predict`, `predict_proba`, `score`.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## 🧠 How ABC Works
|
|
195
|
+
|
|
196
|
+
ABC models the foraging behaviour of a honey bee colony. Each **food source** is a candidate solution, and its **nectar amount** is the solution's fitness.
|
|
197
|
+
|
|
198
|
+
| Phase | Role |
|
|
199
|
+
|---|---|
|
|
200
|
+
| **Employed bees** | Each one improves its own food source by perturbing one dimension towards or away from a random neighbour: `v_ij = x_ij + φ·(x_ij − x_kj)`, `φ ∈ [−1, 1]`. Greedy selection keeps the better solution. |
|
|
201
|
+
| **Onlooker bees** | Pick sources with probability proportional to fitness (roulette wheel), so promising regions are exploited more. |
|
|
202
|
+
| **Scout bees** | A source that has not improved for `limit` trials is abandoned and replaced by a random one, which keeps exploration alive. |
|
|
203
|
+
|
|
204
|
+
Fitness is derived from cost as `1 / (1 + f)` for `f ≥ 0` and `1 + |f|` otherwise.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## 🔬 Case Study: Federated Learning on MedMNIST
|
|
209
|
+
|
|
210
|
+
The library was used to tune a **federated learning** setup on **PathMNIST** (colon pathology images, 9 classes). The data is split across **5 simulated hospitals** with a **Dirichlet distribution (α = 0.5)**, a non-IID setting where FedAvg with default hyperparameters struggles.
|
|
211
|
+
|
|
212
|
+
`CanonicalABCSolver` searched the clients' **learning rate** (0.001–0.1) and **SGD momentum** (0.0–0.9), scoring each candidate with a short 3-round FL simulation as a proxy task. The best configuration was then trained for 15 rounds:
|
|
213
|
+
|
|
214
|
+
| Method | LR | Momentum | Test accuracy (round 15) |
|
|
215
|
+
|---|---|---|---|
|
|
216
|
+
| Standard FedAvg | 0.010 | 0.50 | 78.05% |
|
|
217
|
+
| **ABC-tuned FedAvg** | **0.069** | **0.57** | **85.28%** (+7.2 pts) |
|
|
218
|
+
|
|
219
|
+
For reference, centralized training with the default hyperparameters reached 80.29%.
|
|
220
|
+
|
|
221
|
+

|
|
222
|
+
|
|
223
|
+
📓 [Full notebook and analysis](https://github.com/yusufkorkmazyigit/abc-optimizer-lib/blob/main/examples/Federated_Learning_MedMNIST_Optimization.ipynb) · Related repo: [fedLearningwithABC](https://github.com/yusufkorkmazyigit/fedLearningwithABC)
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 🗺️ Roadmap
|
|
228
|
+
|
|
229
|
+
- [x] Categorical and log-scaled hyperparameters
|
|
230
|
+
- [x] `random_state` for reproducibility
|
|
231
|
+
- [x] Unit tests and continuous integration
|
|
232
|
+
- [x] Parallel cross-validation (`n_jobs`)
|
|
233
|
+
- [ ] Benchmarks against `GridSearchCV`, `RandomizedSearchCV` and Optuna
|
|
234
|
+
- [ ] Parallel evaluation of candidates within a colony
|
|
235
|
+
- [x] PyPI release
|
|
236
|
+
|
|
237
|
+
Contributions and issues are welcome.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## 🧪 Development
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
pip install -e ".[test]"
|
|
245
|
+
pytest
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
See [CHANGELOG.md](https://github.com/yusufkorkmazyigit/abc-optimizer-lib/blob/main/CHANGELOG.md) for release notes.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## 📚 Reference
|
|
253
|
+
|
|
254
|
+
Karaboğa, D. (2005). *An Idea Based on Honey Bee Swarm for Numerical Optimization*. Technical Report TR06, Erciyes University, Engineering Faculty, Computer Engineering Department.
|
|
255
|
+
|
|
256
|
+
If you use this library in your research, please cite the original paper above and link to this repository.
|
|
257
|
+
|
|
258
|
+
## 📝 License
|
|
259
|
+
|
|
260
|
+
Released under the [MIT License](https://github.com/yusufkorkmazyigit/abc-optimizer-lib/blob/main/LICENSE).
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
<sub>Developed by [Yusuf Korkmazyiğit](https://github.com/yusufkorkmazyigit), Computer Engineering, Erciyes University.</sub>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
abcopt/__init__.py,sha256=EDd2gahJX5L4l_SDiHjk6tOABGyaTc-0f4GgNsqZTiM,245
|
|
2
|
+
abcopt/core.py,sha256=6dj41EqrJa1q7A-HjClgzLZ7i3iwUYhxPpIEq5pjp3o,7012
|
|
3
|
+
abcopt/sklearn_wrapper.py,sha256=aXEy3btmRP5CYgEvKk7Z16XW2VawjpsC_Le5PhUxV3o,8505
|
|
4
|
+
abcopt-0.2.0.dist-info/licenses/LICENSE,sha256=wOfJ9vBqnqhMsyLneh-DYtLNV575mO_dk5OibDsBNUs,1076
|
|
5
|
+
abcopt-0.2.0.dist-info/METADATA,sha256=mgEiLDmVEoEbAeCK8nwgeSgR6Ir8zc2Ep3c8xhn0Y14,10417
|
|
6
|
+
abcopt-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
7
|
+
abcopt-0.2.0.dist-info/top_level.txt,sha256=MwHgrEg0Ocm1R6pZKoUo4j6H0LMuYOIm1nvddMZetVk,7
|
|
8
|
+
abcopt-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yusuf Korkmazyiğit
|
|
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 @@
|
|
|
1
|
+
abcopt
|