useful-optimizer 0.1.1__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.
- opt/__init__.py +6 -0
- opt/abstract_optimizer.py +74 -0
- opt/adadelta.py +148 -0
- opt/adagrad.py +111 -0
- opt/adaptive_moment_estimation.py +140 -0
- opt/ant_colony.py +194 -0
- opt/artificial_fish_swarm_algorithm.py +189 -0
- opt/augmented_lagrangian_method.py +177 -0
- opt/bat_algorithm.py +191 -0
- opt/bee_algorithm.py +166 -0
- opt/benchmark/__init__.py +6 -0
- opt/benchmark/functions.py +333 -0
- opt/cat_swarm_optimization.py +188 -0
- opt/cma_es.py +180 -0
- opt/colliding_bodies_optimization.py +107 -0
- opt/cross_entropy_method.py +126 -0
- opt/cuckoo_search.py +142 -0
- opt/cultural_algorithm.py +168 -0
- opt/differential_evolution.py +150 -0
- opt/eagle_strategy.py +113 -0
- opt/estimation_of_distribution_algorithm.py +164 -0
- opt/firefly_algorithm.py +144 -0
- opt/genetic_algorithm.py +192 -0
- opt/glowworm_swarm_optimization.py +204 -0
- opt/grey_wolf_optimizer.py +127 -0
- opt/harmony_search.py +172 -0
- opt/hill_climbing.py +140 -0
- opt/imperialist_competitive_algorithm.py +198 -0
- opt/linear_discriminant_analysis.py +201 -0
- opt/particle_filter.py +151 -0
- opt/particle_swarm.py +170 -0
- opt/parzen_tree_stimator.py +190 -0
- opt/shuffled_frog_leaping_algorithm.py +151 -0
- opt/simulated_annealing.py +155 -0
- opt/sine_cosine_algorithm.py +206 -0
- opt/squirrel_search.py +113 -0
- opt/stochastic_diffusion_search.py +154 -0
- opt/stochastic_fractal_search.py +178 -0
- opt/successive_linear_programming.py +112 -0
- opt/tabu_search.py +192 -0
- opt/variable_depth_search.py +138 -0
- opt/variable_neighbourhood_search.py +142 -0
- opt/very_large_scale_neighborhood_search.py +140 -0
- opt/whale_optimization_algorithm.py +144 -0
- useful_optimizer-0.1.1.dist-info/LICENSE +21 -0
- useful_optimizer-0.1.1.dist-info/METADATA +163 -0
- useful_optimizer-0.1.1.dist-info/RECORD +48 -0
- useful_optimizer-0.1.1.dist-info/WHEEL +4 -0
opt/__init__.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""This module defines an abstract base class for optimizers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC
|
|
6
|
+
from abc import abstractmethod
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
|
|
15
|
+
from numpy import ndarray
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AbstractOptimizer(ABC):
|
|
19
|
+
"""An abstract base class for optimizers.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
func (Callable): The objective function to be optimized.
|
|
23
|
+
lower_bound (float): The lower bound of the search space.
|
|
24
|
+
upper_bound (float): The upper bound of the search space.
|
|
25
|
+
dim (int): The dimensionality of the search space.
|
|
26
|
+
max_iter (int, optional): The maximum number of iterations for the optimization process. Defaults to 1000.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
func (Callable): The objective function to be optimized.
|
|
30
|
+
lower_bound (float): The lower bound of the search space.
|
|
31
|
+
upper_bound (float): The upper bound of the search space.
|
|
32
|
+
dim (int): The dimensionality of the search space.
|
|
33
|
+
max_iter (int): The maximum number of iterations for the optimization process.
|
|
34
|
+
seed (int): The seed for the random number generator.
|
|
35
|
+
population_size (int): The number of individuals in the population.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
Methods:
|
|
39
|
+
search() -> Tuple[TBestSolution, TBestFitness]:
|
|
40
|
+
Perform the optimization search.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Tuple containing the best solution found and its corresponding fitness value.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
func: Callable[[ndarray], float],
|
|
49
|
+
lower_bound: float,
|
|
50
|
+
upper_bound: float,
|
|
51
|
+
dim: int,
|
|
52
|
+
max_iter: int = 1000,
|
|
53
|
+
seed: int | None = None,
|
|
54
|
+
population_size: int = 100,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Initialize the optimizer."""
|
|
57
|
+
self.func = func
|
|
58
|
+
self.lower_bound = lower_bound
|
|
59
|
+
self.upper_bound = upper_bound
|
|
60
|
+
self.dim = dim
|
|
61
|
+
self.max_iter = max_iter
|
|
62
|
+
if seed is None:
|
|
63
|
+
self.seed = np.random.default_rng(42).integers(0, 2**32)
|
|
64
|
+
else:
|
|
65
|
+
self.seed = seed
|
|
66
|
+
self.population_size = population_size
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def search(self) -> tuple[np.ndarray, float]:
|
|
70
|
+
"""Perform the optimization search.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Tuple containing the best solution found and its corresponding fitness value.
|
|
74
|
+
"""
|
opt/adadelta.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""AdaDelta Optimizer.
|
|
2
|
+
|
|
3
|
+
This module implements the AdaDelta optimizer, which is an extension of AdaGrad that
|
|
4
|
+
seeks to reduce its sensitivity to the learning rate hyperparameter.
|
|
5
|
+
|
|
6
|
+
AdaDelta is a gradient-based optimization algorithm that adapts the learning rate
|
|
7
|
+
for each of the parameters in the model. It is designed to converge faster than
|
|
8
|
+
AdaGrad by using a moving average of the squared gradient values to scale the learning rate.
|
|
9
|
+
|
|
10
|
+
The AdaDelta optimizer is defined by the following update rule:
|
|
11
|
+
|
|
12
|
+
Eg = rho * Eg + (1 - rho) * g^2
|
|
13
|
+
dx = -sqrt(Edx + eps) / sqrt(Eg + eps) * g
|
|
14
|
+
Edx = rho * Edx + (1 - rho) * dx^2
|
|
15
|
+
x = x + dx
|
|
16
|
+
|
|
17
|
+
where:
|
|
18
|
+
- x: current solution
|
|
19
|
+
- g: gradient of the objective function
|
|
20
|
+
- rho: decay rate
|
|
21
|
+
- eps: small constant to avoid dividing by zero
|
|
22
|
+
- Eg: moving average of squared gradient values
|
|
23
|
+
- Edx: moving average of squared updates
|
|
24
|
+
|
|
25
|
+
The algorithm iteratively updates the solution x by computing the gradient of the
|
|
26
|
+
objective function at x, scaling it by the moving average of the squared gradients,
|
|
27
|
+
and dividing it by the square root of the moving average of the squared updates.
|
|
28
|
+
|
|
29
|
+
The algorithm continues for a fixed number of iterations or until a specified
|
|
30
|
+
stopping criterion is met, returning the best solution found.
|
|
31
|
+
|
|
32
|
+
This module provides a simple example of how to use the AdaDelta optimizer to minimize
|
|
33
|
+
the Shifted Ackley's function in two dimensions.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
$ python adadelta.py
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
rho (float): Decay rate for the moving average of squared gradients
|
|
40
|
+
eps (float): Small constant to avoid dividing by zero
|
|
41
|
+
Eg (np.ndarray): Moving average of squared gradient values
|
|
42
|
+
Edx (np.ndarray): Moving average of squared updates
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
from typing import TYPE_CHECKING
|
|
48
|
+
|
|
49
|
+
import numpy as np
|
|
50
|
+
|
|
51
|
+
from scipy.optimize import approx_fprime
|
|
52
|
+
|
|
53
|
+
from opt.abstract_optimizer import AbstractOptimizer
|
|
54
|
+
from opt.benchmark.functions import shifted_ackley
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if TYPE_CHECKING:
|
|
58
|
+
from collections.abc import Callable
|
|
59
|
+
|
|
60
|
+
from numpy import ndarray
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AdaDelta(AbstractOptimizer):
|
|
64
|
+
"""AdaDelta optimizer implementation.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
func (Callable[[ndarray], float]): The objective function to be minimized.
|
|
68
|
+
lower_bound (float): The lower bound of the search space.
|
|
69
|
+
upper_bound (float): The upper bound of the search space.
|
|
70
|
+
dim (int): The dimensionality of the search space.
|
|
71
|
+
max_iter (int, optional): The maximum number of iterations. Defaults to 1000.
|
|
72
|
+
seed (Optional[int], optional): The seed value for random number generation. Defaults to None.
|
|
73
|
+
rho (float, optional): The decay rate for the moving average of squared gradients. Defaults to 0.97.
|
|
74
|
+
eps (float, optional): A small constant to avoid division by zero. Defaults to 1e-8.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
func: Callable[[ndarray], float],
|
|
80
|
+
lower_bound: float,
|
|
81
|
+
upper_bound: float,
|
|
82
|
+
dim: int,
|
|
83
|
+
max_iter: int = 1000,
|
|
84
|
+
rho: float = 0.97,
|
|
85
|
+
eps: float = 1e-8,
|
|
86
|
+
seed: int | None = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Initialize the AdaDelta optimizer."""
|
|
89
|
+
super().__init__(
|
|
90
|
+
func=func,
|
|
91
|
+
lower_bound=lower_bound,
|
|
92
|
+
upper_bound=upper_bound,
|
|
93
|
+
dim=dim,
|
|
94
|
+
max_iter=max_iter,
|
|
95
|
+
seed=seed,
|
|
96
|
+
)
|
|
97
|
+
self.rho = rho
|
|
98
|
+
self.eps = eps
|
|
99
|
+
self.Eg = np.zeros(self.dim)
|
|
100
|
+
self.Edx = np.zeros(self.dim)
|
|
101
|
+
|
|
102
|
+
def _update(self, x: np.ndarray) -> np.ndarray:
|
|
103
|
+
"""Perform a single update step of the AdaDelta algorithm.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
x (np.ndarray): The current solution.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
np.ndarray: The updated solution.
|
|
110
|
+
"""
|
|
111
|
+
g = approx_fprime(x, self.func, self.eps)
|
|
112
|
+
self.Eg = self.rho * self.Eg + (1 - self.rho) * g**2
|
|
113
|
+
dx = -np.sqrt(self.Edx + self.eps) / np.sqrt(self.Eg + self.eps) * g
|
|
114
|
+
self.Edx = self.rho * self.Edx + (1 - self.rho) * dx**2
|
|
115
|
+
return x + dx
|
|
116
|
+
|
|
117
|
+
def search(self) -> tuple[np.ndarray, float]:
|
|
118
|
+
"""Perform the AdaDelta search.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Tuple[np.ndarray, float]: A tuple containing the best solution found and its fitness value.
|
|
122
|
+
"""
|
|
123
|
+
# Initialize solution
|
|
124
|
+
x = np.random.default_rng(self.seed).uniform(
|
|
125
|
+
self.lower_bound, self.upper_bound, self.dim
|
|
126
|
+
)
|
|
127
|
+
best_solution = x
|
|
128
|
+
best_fitness = np.inf
|
|
129
|
+
|
|
130
|
+
# Main loop
|
|
131
|
+
for _ in range(self.max_iter):
|
|
132
|
+
x = self._update(x)
|
|
133
|
+
x = np.clip(x, self.lower_bound, self.upper_bound)
|
|
134
|
+
fitness = self.func(x)
|
|
135
|
+
if fitness < best_fitness:
|
|
136
|
+
best_fitness = fitness
|
|
137
|
+
best_solution = x
|
|
138
|
+
|
|
139
|
+
return best_solution, best_fitness
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
optimizer = AdaDelta(
|
|
144
|
+
func=shifted_ackley, lower_bound=-2.768, upper_bound=+2.768, dim=2
|
|
145
|
+
)
|
|
146
|
+
best_solution, best_fitness = optimizer.search()
|
|
147
|
+
print(f"Best solution found: {best_solution}")
|
|
148
|
+
print(f"Best fitness value: {best_fitness}")
|
opt/adagrad.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""ADAGrad Optimizer.
|
|
2
|
+
|
|
3
|
+
This module implements the Adaptive Gradient Algorithm (ADAGrad) optimizer. ADAGrad is
|
|
4
|
+
a gradient-based optimization algorithm that adapts the learning rate to the parameters,
|
|
5
|
+
performing smaller updates for parameters associated with frequently occurring features,
|
|
6
|
+
and larger updates for parameters associated with infrequent features. It is particularly
|
|
7
|
+
useful for dealing with sparse data.
|
|
8
|
+
|
|
9
|
+
ADAGrad's main strength is that it eliminates the need to manually tune the learning rate.
|
|
10
|
+
Most implementations also include a 'smoothing term' to avoid division by zero when the
|
|
11
|
+
gradient is zero.
|
|
12
|
+
|
|
13
|
+
The ADAGrad optimizer is commonly used in machine learning and deep learning applications.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
optimizer = ADAGrad(func=objective_function, learning_rate=0.01, initial_guess=[0, 0])
|
|
17
|
+
best_solution, best_fitness = optimizer.optimize()
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
func (Callable): The objective function to optimize.
|
|
21
|
+
learning_rate (float): The learning rate for the optimization.
|
|
22
|
+
initial_guess (List[float]): The starting point for the optimization.
|
|
23
|
+
|
|
24
|
+
Methods:
|
|
25
|
+
optimize(): Perform the ADAGrad optimization.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from typing import TYPE_CHECKING
|
|
31
|
+
|
|
32
|
+
import numpy as np
|
|
33
|
+
|
|
34
|
+
from scipy.optimize import approx_fprime
|
|
35
|
+
|
|
36
|
+
from opt.abstract_optimizer import AbstractOptimizer
|
|
37
|
+
from opt.benchmark.functions import shifted_ackley
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from collections.abc import Callable
|
|
42
|
+
|
|
43
|
+
from numpy import ndarray
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ADAGrad(AbstractOptimizer):
|
|
47
|
+
"""ADAGrad optimizer implementation.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
func (Callable[[ndarray], float]): The objective function to be optimized.
|
|
51
|
+
lower_bound (float): The lower bound of the search space.
|
|
52
|
+
upper_bound (float): The upper bound of the search space.
|
|
53
|
+
dim (int): The dimensionality of the search space.
|
|
54
|
+
max_iter (int, optional): The maximum number of iterations. Defaults to 1000.
|
|
55
|
+
seed (Optional[int], optional): The seed value for random number generation. Defaults to None.
|
|
56
|
+
lr (float, optional): The learning rate. Defaults to 0.01.
|
|
57
|
+
eps (float, optional): A small value added to the denominator for numerical stability. Defaults to 1e-8.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
func: Callable[[ndarray], float],
|
|
63
|
+
lower_bound: float,
|
|
64
|
+
upper_bound: float,
|
|
65
|
+
dim: int,
|
|
66
|
+
max_iter: int = 1000,
|
|
67
|
+
lr: float = 0.01,
|
|
68
|
+
eps: float = 1e-8,
|
|
69
|
+
seed: int | None = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Initialize the ADAGrad optimizer."""
|
|
72
|
+
super().__init__(
|
|
73
|
+
func=func,
|
|
74
|
+
lower_bound=lower_bound,
|
|
75
|
+
upper_bound=upper_bound,
|
|
76
|
+
dim=dim,
|
|
77
|
+
max_iter=max_iter,
|
|
78
|
+
seed=seed,
|
|
79
|
+
)
|
|
80
|
+
self.lr = lr
|
|
81
|
+
self.eps = eps
|
|
82
|
+
|
|
83
|
+
def search(self) -> tuple[np.ndarray, float]:
|
|
84
|
+
"""Perform the ADAGrad search algorithm.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Tuple[np.ndarray, float]: A tuple containing the best solution found and its fitness value.
|
|
88
|
+
"""
|
|
89
|
+
x = np.random.default_rng(self.seed).uniform(
|
|
90
|
+
self.lower_bound, self.upper_bound, self.dim
|
|
91
|
+
)
|
|
92
|
+
grad_accumulator = np.zeros(self.dim)
|
|
93
|
+
|
|
94
|
+
for _ in range(self.max_iter):
|
|
95
|
+
grad = approx_fprime(x, self.func, np.sqrt(np.finfo(float).eps))
|
|
96
|
+
grad_accumulator += grad**2
|
|
97
|
+
adjusted_grad = grad / (np.sqrt(grad_accumulator) + self.eps)
|
|
98
|
+
x = x - self.lr * adjusted_grad
|
|
99
|
+
|
|
100
|
+
best_solution = x
|
|
101
|
+
best_fitness = self.func(best_solution)
|
|
102
|
+
return best_solution, best_fitness
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
optimizer = ADAGrad(
|
|
107
|
+
func=shifted_ackley, lower_bound=-2.768, upper_bound=2.768, dim=2
|
|
108
|
+
)
|
|
109
|
+
best_solution, best_fitness = optimizer.search()
|
|
110
|
+
print(f"Best solution found: {best_solution}")
|
|
111
|
+
print(f"Best fitness value: {best_fitness}")
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Adaptive Moment Estimation (Adam) Optimizer.
|
|
2
|
+
|
|
3
|
+
This module implements the Adam optimization algorithm. Adam is a gradient-based
|
|
4
|
+
optimization algorithm that computes adaptive learning rates for each parameter. It
|
|
5
|
+
combines the advantages of two other extensions of stochastic gradient descent:
|
|
6
|
+
|
|
7
|
+
- AdaGrad
|
|
8
|
+
- RMSProp
|
|
9
|
+
|
|
10
|
+
Adam works well in practice and compares favorably to other adaptive learning-method
|
|
11
|
+
algorithms as it converges fast and the learning speed of the Model is quite fast and
|
|
12
|
+
efficient. It is straightforward to implement, is computationally efficient, has little
|
|
13
|
+
memory requirements, is invariant to diagonal rescaling of the gradients, and is well
|
|
14
|
+
suited for problems that are large in terms of data and/or parameters.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
optimizer = Adam(func=objective_function, learning_rate=0.01, initial_guess=[0, 0])
|
|
18
|
+
best_solution, best_fitness = optimizer.optimize()
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
func (Callable): The objective function to optimize.
|
|
22
|
+
learning_rate (float): The learning rate for the optimization.
|
|
23
|
+
initial_guess (List[float]): The starting point for the optimization.
|
|
24
|
+
|
|
25
|
+
Methods:
|
|
26
|
+
optimize(): Perform the Adam optimization.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from typing import TYPE_CHECKING
|
|
32
|
+
|
|
33
|
+
import numpy as np
|
|
34
|
+
|
|
35
|
+
from scipy.optimize import approx_fprime
|
|
36
|
+
|
|
37
|
+
from opt.abstract_optimizer import AbstractOptimizer
|
|
38
|
+
from opt.benchmark.functions import shifted_ackley
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from collections.abc import Callable
|
|
43
|
+
|
|
44
|
+
from numpy import ndarray
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ADAMOptimization(AbstractOptimizer):
|
|
48
|
+
"""ADAMOptimization is a class that implements the ADAM optimization algorithm.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
func (Callable[[ndarray], float]): The objective function to be optimized.
|
|
52
|
+
lower_bound (float): The lower bound of the search space.
|
|
53
|
+
upper_bound (float): The upper bound of the search space.
|
|
54
|
+
dim (int): The dimensionality of the search space.
|
|
55
|
+
max_iter (int, optional): The maximum number of iterations. Defaults to 1000.
|
|
56
|
+
alpha (float, optional): The learning rate. Defaults to 0.001.
|
|
57
|
+
beta1 (float, optional): The exponential decay rate for the first moment estimates. Defaults to 0.9.
|
|
58
|
+
beta2 (float, optional): The exponential decay rate for the second moment estimates. Defaults to 0.999.
|
|
59
|
+
epsilon (float, optional): A small value added to the denominator for numerical stability. Defaults to 1e-13.
|
|
60
|
+
seed (Optional[int], optional): The random seed for reproducibility. Defaults to None.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
func: Callable[[ndarray], float],
|
|
66
|
+
lower_bound: float,
|
|
67
|
+
upper_bound: float,
|
|
68
|
+
dim: int,
|
|
69
|
+
max_iter: int = 1000,
|
|
70
|
+
alpha: float = 0.001,
|
|
71
|
+
beta1: float = 0.9,
|
|
72
|
+
beta2: float = 0.999,
|
|
73
|
+
epsilon: float = 1e-13,
|
|
74
|
+
seed: int | None = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Initialize the ADAM optimization algorithm."""
|
|
77
|
+
super().__init__(
|
|
78
|
+
func=func,
|
|
79
|
+
lower_bound=lower_bound,
|
|
80
|
+
upper_bound=upper_bound,
|
|
81
|
+
dim=dim,
|
|
82
|
+
max_iter=max_iter,
|
|
83
|
+
seed=seed,
|
|
84
|
+
)
|
|
85
|
+
self.alpha = alpha
|
|
86
|
+
self.beta1 = beta1
|
|
87
|
+
self.beta2 = beta2
|
|
88
|
+
self.epsilon = epsilon
|
|
89
|
+
|
|
90
|
+
def search(self) -> tuple[np.ndarray, float]:
|
|
91
|
+
"""Perform the ADAM optimization search.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Tuple[np.ndarray, float]: A tuple containing the best solution found and its fitness value.
|
|
95
|
+
"""
|
|
96
|
+
m = np.zeros(self.dim)
|
|
97
|
+
v = np.zeros(self.dim)
|
|
98
|
+
best_solution = np.random.default_rng(self.seed).uniform(
|
|
99
|
+
self.lower_bound, self.upper_bound, self.dim
|
|
100
|
+
)
|
|
101
|
+
best_fitness = self.func(best_solution)
|
|
102
|
+
|
|
103
|
+
for t in range(1, self.max_iter + 1):
|
|
104
|
+
grad = self._compute_gradient(best_solution)
|
|
105
|
+
m = self.beta1 * m + (1 - self.beta1) * grad
|
|
106
|
+
v = self.beta2 * v + (1 - self.beta2) * np.square(grad)
|
|
107
|
+
m_hat = m / (1 - np.power(self.beta1, t))
|
|
108
|
+
v_hat = v / (1 - np.power(self.beta2, t))
|
|
109
|
+
|
|
110
|
+
best_solution = best_solution - self.alpha * m_hat / (
|
|
111
|
+
np.sqrt(v_hat) + self.epsilon
|
|
112
|
+
)
|
|
113
|
+
best_solution = np.clip(best_solution, self.lower_bound, self.upper_bound)
|
|
114
|
+
|
|
115
|
+
fitness = self.func(best_solution)
|
|
116
|
+
if fitness < best_fitness:
|
|
117
|
+
best_fitness = fitness
|
|
118
|
+
|
|
119
|
+
return best_solution, best_fitness
|
|
120
|
+
|
|
121
|
+
def _compute_gradient(self, x: np.ndarray) -> np.ndarray:
|
|
122
|
+
"""Compute the gradient of the objective function at a given point.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
x (np.ndarray): The point at which to compute the gradient.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
np.ndarray: The gradient vector.
|
|
129
|
+
"""
|
|
130
|
+
epsilon = np.sqrt(np.finfo(float).eps)
|
|
131
|
+
return approx_fprime(x, self.func, epsilon)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
optimizer = ADAMOptimization(
|
|
136
|
+
func=shifted_ackley, lower_bound=-2.768, upper_bound=+2.768, dim=2
|
|
137
|
+
)
|
|
138
|
+
best_solution, best_fitness = optimizer.search()
|
|
139
|
+
print(f"Best solution: {best_solution}")
|
|
140
|
+
print(f"Best fitness: {best_fitness}")
|
opt/ant_colony.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Ant Colony Optimization (ACO) Algorithm.
|
|
2
|
+
|
|
3
|
+
This module implements the Ant Colony Optimization (ACO) algorithm. ACO is a
|
|
4
|
+
population-based metaheuristic that can be used to find approximate solutions to
|
|
5
|
+
difficult optimization problems.
|
|
6
|
+
|
|
7
|
+
In ACO, a set of software agents called artificial ants search for good solutions to a
|
|
8
|
+
given optimization problem. To apply ACO, the optimization problem is transformed into
|
|
9
|
+
the problem of finding the best path on a weighted graph. The artificial ants
|
|
10
|
+
incrementally build solutions by moving on the graph. The solution construction process
|
|
11
|
+
is stochastic and is biased by a pheromone model, that is, a set of parameters
|
|
12
|
+
associated with graph components (either nodes or edges) whose values are modified
|
|
13
|
+
at runtime by the ants.
|
|
14
|
+
|
|
15
|
+
ACO is particularly useful for problems that can be reduced to finding paths on
|
|
16
|
+
weighted graphs, like the traveling salesman problem, the vehicle routing problem, and
|
|
17
|
+
the quadratic assignment problem.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
optimizer = AntColony(func=objective_function, lower_bound=-10, upper_bound=10,
|
|
21
|
+
dim=2, n_ants=50, max_iter=1000)
|
|
22
|
+
best_solution, best_fitness = optimizer.search()
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
func (Callable): The objective function to optimize.
|
|
26
|
+
lower_bound (float): The lower bound of the search space.
|
|
27
|
+
upper_bound (float): The upper bound of the search space.
|
|
28
|
+
dim (int): The dimension of the search space.
|
|
29
|
+
n_ants (int): The number of ants (candidate solutions).
|
|
30
|
+
max_iter (int): The maximum number of iterations.
|
|
31
|
+
|
|
32
|
+
Methods:
|
|
33
|
+
search(): Perform the ACO optimization.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from typing import TYPE_CHECKING
|
|
39
|
+
|
|
40
|
+
import numpy as np
|
|
41
|
+
|
|
42
|
+
from opt.abstract_optimizer import AbstractOptimizer
|
|
43
|
+
from opt.benchmark.functions import shifted_ackley
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from collections.abc import Callable
|
|
48
|
+
|
|
49
|
+
from numpy import ndarray
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AntColony(AbstractOptimizer):
|
|
53
|
+
"""Ant Colony Optimization algorithm for optimization problems.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
func (Callable[[ndarray], float]): The objective function to be minimized.
|
|
57
|
+
lower_bound (float): The lower bound of the search space.
|
|
58
|
+
upper_bound (float): The upper bound of the search space.
|
|
59
|
+
dim (int): The dimensionality of the search space.
|
|
60
|
+
max_iter (int, optional): The maximum number of iterations. Defaults to 1000.
|
|
61
|
+
population_size (int, optional): The number of ants in the colony. Defaults to 100.
|
|
62
|
+
seed (Optional[int], optional): The seed value for random number generation. Defaults to None.
|
|
63
|
+
alpha (float, optional): The importance of pheromone in the solution construction. Defaults to 1.
|
|
64
|
+
beta (float, optional): The importance of heuristic information in the solution construction. Defaults to 1.
|
|
65
|
+
rho (float, optional): The pheromone evaporation rate. Defaults to 0.5.
|
|
66
|
+
q (float, optional): The pheromone deposit factor. Defaults to 1.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
alpha (float): The importance of pheromone in the solution construction.
|
|
70
|
+
beta (float): The importance of heuristic information in the solution construction.
|
|
71
|
+
rho (float): The pheromone evaporation rate.
|
|
72
|
+
q (float): The pheromone deposit factor.
|
|
73
|
+
ants (np.ndarray): The current solutions generated by the ants.
|
|
74
|
+
pheromone (np.ndarray): The pheromone matrix.
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
func: Callable[[ndarray], float],
|
|
81
|
+
lower_bound: float,
|
|
82
|
+
upper_bound: float,
|
|
83
|
+
dim: int,
|
|
84
|
+
max_iter: int = 1000,
|
|
85
|
+
population_size: int = 100,
|
|
86
|
+
seed: int | None = None,
|
|
87
|
+
alpha: float = 1,
|
|
88
|
+
beta: float = 1,
|
|
89
|
+
rho: float = 0.5,
|
|
90
|
+
q: float = 1,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Initialize the Ant Colony Optimization algorithm."""
|
|
93
|
+
super().__init__(
|
|
94
|
+
func=func,
|
|
95
|
+
lower_bound=lower_bound,
|
|
96
|
+
upper_bound=upper_bound,
|
|
97
|
+
dim=dim,
|
|
98
|
+
max_iter=max_iter,
|
|
99
|
+
seed=seed,
|
|
100
|
+
population_size=population_size,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
self.alpha = alpha
|
|
104
|
+
self.beta = beta
|
|
105
|
+
self.rho = rho
|
|
106
|
+
self.q = q
|
|
107
|
+
self.ants = np.random.default_rng(self.seed).uniform(
|
|
108
|
+
self.lower_bound, self.upper_bound, (self.population_size, self.dim)
|
|
109
|
+
)
|
|
110
|
+
self.pheromone = np.ones((self.population_size, self.dim))
|
|
111
|
+
|
|
112
|
+
def search(self) -> tuple[np.ndarray, float]:
|
|
113
|
+
"""Run the Ant Colony Optimization algorithm.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Tuple[np.ndarray, float]: The best solution found and its corresponding fitness value.
|
|
117
|
+
|
|
118
|
+
"""
|
|
119
|
+
best_fitness = np.inf
|
|
120
|
+
best_solution = None
|
|
121
|
+
|
|
122
|
+
for _ in range(self.max_iter):
|
|
123
|
+
for i in range(self.population_size):
|
|
124
|
+
solution = self.ants[i]
|
|
125
|
+
fitness = self.func(solution)
|
|
126
|
+
|
|
127
|
+
if fitness < best_fitness:
|
|
128
|
+
best_fitness = fitness
|
|
129
|
+
best_solution = solution
|
|
130
|
+
|
|
131
|
+
self.update_pheromone(i, fitness)
|
|
132
|
+
self.ants[i] = self.generate_new_solution(i)
|
|
133
|
+
|
|
134
|
+
# Local search
|
|
135
|
+
for i in range(self.population_size):
|
|
136
|
+
local_best = self.local_search(self.ants[i])
|
|
137
|
+
local_fitness = self.func(local_best)
|
|
138
|
+
|
|
139
|
+
if local_fitness < best_fitness:
|
|
140
|
+
best_fitness = local_fitness
|
|
141
|
+
best_solution = local_best
|
|
142
|
+
|
|
143
|
+
return best_solution, best_fitness
|
|
144
|
+
|
|
145
|
+
def update_pheromone(self, i: int, fitness: float) -> None:
|
|
146
|
+
"""Update the pheromone matrix based on the fitness of the ant's solution.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
i (int): The index of the ant.
|
|
150
|
+
fitness (float): The fitness value of the ant's solution.
|
|
151
|
+
|
|
152
|
+
"""
|
|
153
|
+
self.pheromone[i] = (1 - self.rho) * self.pheromone[i] + self.rho * (
|
|
154
|
+
self.q / fitness
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def generate_new_solution(self, i: int) -> np.ndarray:
|
|
158
|
+
"""Generate a new solution for the ant.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
i (int): The index of the ant.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
np.ndarray: The new solution generated for the ant.
|
|
165
|
+
|
|
166
|
+
"""
|
|
167
|
+
new_solution = self.ants[i] + self.pheromone[
|
|
168
|
+
i
|
|
169
|
+
] ** self.alpha * np.random.default_rng(self.seed).uniform(-1, 1, self.dim)
|
|
170
|
+
return np.clip(new_solution, self.lower_bound, self.upper_bound)
|
|
171
|
+
|
|
172
|
+
def local_search(self, solution: np.ndarray) -> np.ndarray:
|
|
173
|
+
"""Perform a local search by adding a small perturbation to the solution.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
solution (np.ndarray): The solution to perform local search on.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
np.ndarray: The new solution after local search.
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
new_solution = solution + np.random.default_rng(self.seed).uniform(
|
|
183
|
+
-0.01, 0.01, self.dim
|
|
184
|
+
)
|
|
185
|
+
return np.clip(new_solution, self.lower_bound, self.upper_bound)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
if __name__ == "__main__":
|
|
189
|
+
optimizer = AntColony(
|
|
190
|
+
func=shifted_ackley, lower_bound=-2.768, upper_bound=+2.768, dim=2
|
|
191
|
+
)
|
|
192
|
+
best_solution, best_fitness = optimizer.search()
|
|
193
|
+
print(f"Best solution found: {best_solution}")
|
|
194
|
+
print(f"Best fitness value: {best_fitness}")
|