├── .gitignore ├── LICENSE ├── README.md ├── models ├── single_objective │ ├── __init__.py │ ├── bat_search.py │ ├── cuckoo_search.py │ ├── firefly_algorithm.py │ ├── gravitational_search.py │ ├── harmony_search.py │ └── pso.py └── swarm_algorithm.py ├── notebooks └── swarm_algorithms_demo.ipynb ├── requirements.txt └── utils ├── __init__.py ├── optimization_visualization.py └── test_functions.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled 2 | __pycache__/ 3 | 4 | # Visual Studio Code config 5 | .vscode 6 | 7 | # Enviroment 8 | /venv 9 | 10 | # Jupyter Notebook 11 | .ipynb_checkpoints -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alexander Klanovets 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swarm intelligence algorithms implementation 2 | 3 | The algorithms developed in this repository are part of my Bachelor's diploma thesis dedicated to designing a system based on multicriteria swarm algorithms for tuning artificial neural networks. 4 | 5 | ### Repository contents 6 | - an implementation of different swarm intelligence algorithms (for now, specifically for unconstrained continuous optimization problems with a single objective); 7 | - a set of test objective functions to experiment with and use them for models' tests; 8 | - a utility for optimization process visualization; 9 | - a Jupyter notebook with a usage and visualization demo (animations don't work properly on a Github Jupyter notebook viewer). 10 | 11 | ### Feel free to comment the code (especially if you find something incorrect). -------------------------------------------------------------------------------- /models/single_objective/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OleksandrKlanovets/swarm_algorithms/62355a29570a6978943e243538d25365dc3b6cdd/models/single_objective/__init__.py -------------------------------------------------------------------------------- /models/single_objective/bat_search.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import math 3 | from collections import namedtuple 4 | from models.swarm_algorithm import SwarmAlgorithm 5 | 6 | 7 | BatSearchParams = namedtuple( 8 | 'BatSearchParams', 9 | ['fmin', 'fmax', 'sigma', 'A_0', 'r_0', 'alpha', 'gamma'] 10 | ) 11 | 12 | 13 | class BatSearch(SwarmAlgorithm): 14 | ''' 15 | Bat Search algorithm. 16 | 17 | Parameters 18 | ---------- 19 | D : int 20 | Search space dimension. 21 | N : int 22 | Population size. 23 | fit_func : callable 24 | Fitness (objective) function. 25 | params : BatSearchParams 26 | Model behavioral parameters. 27 | bounds : ndarray 28 | A 2 by D matrix containing lower and upper bounds of the search space 29 | for each dimension. 30 | seed : int, optional, default=None 31 | Random generator seed. 32 | max_iter : int, optional, default=100 33 | Maximum number of iterations (generations). 34 | stag_iter : int, optional, default=100 35 | Specifies the allowed number of iterations without solution improvement 36 | by equal or more than a given tolerance. If the number is exceeded, 37 | the optimization process stagnations occurs and the algorithm stops. 38 | e : float, optional, default=1e-5 39 | Tolerance. 40 | 41 | Attributes 42 | ---------- 43 | fmin : float 44 | Minimal frequency. 45 | fmax : float 46 | Maximum frequency. 47 | sigma : float 48 | Step. 49 | A_0 : float 50 | Initial loudness. 51 | r_0 : float 52 | Initial rate of pulse emission. 53 | alpha : float 54 | A coefficient used to decrease loudness. 55 | gamma : float 56 | A coefficient used to decrease the rate of pulse emission. 57 | particles : ndarray 58 | An N by D array representing the swarm of N particles. 59 | scores : ndarray 60 | An array of size N representing the value of the fitness function 61 | for each particle. 62 | gbest : ndarray 63 | A D-dimensional vector representing the position of the current 64 | global best particle. 65 | gbest_score : float 66 | The value of the fitness function for the current global best particle. 67 | eval_num : int 68 | The number of fitness function evaluations. 69 | ''' 70 | def __init__(self, D, N, fit_func, params, bounds, seed=None, max_iter=100, 71 | stag_iter=100, e=0.00001): 72 | super().__init__(D, N, fit_func, params, bounds, seed, max_iter, 73 | stag_iter, e) 74 | 75 | def set_population(self, new_population): 76 | ''' 77 | Sets a population with a pre-generated one. 78 | 79 | Parameters 80 | ---------- 81 | new_population: array_like 82 | A matrix with dimensions N by D, which represents the coordinates 83 | of each particle. 84 | 85 | Returns 86 | ------- 87 | No value. 88 | ''' 89 | SwarmAlgorithm.set_population(self, new_population) 90 | 91 | self.velocities = np.zeros((self.N, self.D)) 92 | self.frequencies = np.zeros(self.N) 93 | self.loudness = np.full(self.N, self.A_0) 94 | self.rates = np.full(self.N, self.r_0) 95 | 96 | def set_params(self, new_params): 97 | ''' 98 | Initialize the algorithm with a strategy (vector of parameters). 99 | 100 | Parameters 101 | ---------- 102 | new_params : BatSearchParams 103 | 104 | Returns 105 | ------- 106 | No value. 107 | ''' 108 | self.fmin = new_params.fmin 109 | self.fmax = new_params.fmax 110 | self.sigma = new_params.sigma 111 | self.A_0 = new_params.A_0 112 | self.r_0 = new_params.r_0 113 | self.alpha = new_params.alpha 114 | self.gamma = new_params.gamma 115 | 116 | def __move_all(self): 117 | ''' 118 | Updates the positions of all the bats in the swarm. 119 | 120 | Parameters 121 | ---------- 122 | No parameters. 123 | 124 | Returns 125 | ------- 126 | No value. 127 | ''' 128 | freq_range = self.fmax - self.fmin 129 | rand_coef = np.random.uniform(low=0, high=1, size=(self.N, 1)) 130 | self.frequencies = self.fmin + freq_range * rand_coef 131 | self.velocities += (self.particles - self.gbest) * self.frequencies 132 | new_solutions = self.particles + self.velocities 133 | self.simplebounds(new_solutions) 134 | return new_solutions 135 | 136 | def __local_search(self, new_solutions): 137 | ''' 138 | Performs local search around the global best particle. 139 | 140 | Parameters 141 | ---------- 142 | new_solutions : ndarray 143 | An array of D-dimensional vectors, which represent new solutions. 144 | 145 | Returns 146 | ------- 147 | No value. 148 | ''' 149 | apply_indices = np.random.rand(self.N) > self.rates 150 | apply_size = np.sum(apply_indices) 151 | 152 | rand_coefs = np.random.normal(size=(apply_size, self.D)) 153 | new_solutions[apply_indices] = np.copy(self.gbest) 154 | new_solutions[apply_indices] += self.sigma * rand_coefs 155 | new_solutions[apply_indices] *= np.mean(self.loudness) 156 | self.simplebounds(new_solutions[apply_indices]) 157 | 158 | def optimize(self): 159 | ''' 160 | Main loop of the algorithm. 161 | 162 | Parameters 163 | ---------- 164 | No parameters. 165 | 166 | Returns 167 | ------- 168 | ndarray 169 | The coordinates of the global best particle at the end of 170 | the optimization process. 171 | ''' 172 | i = 0 173 | stag_count = 0 174 | prev_best_score = self.gbest_score 175 | 176 | # MAIN LOOP 177 | while (i < self.max_iter) and (stag_count < self.stag_iter): 178 | new_solutions = self.__move_all() 179 | self.__local_search(new_solutions) 180 | new_scores = np.array([self.fit_func(s) for s in new_solutions]) 181 | scores_condition = new_scores < self.scores 182 | loudness_condition = np.random.rand(self.N) < self.loudness 183 | apply_indices = np.logical_and(scores_condition, loudness_condition) 184 | self.particles[apply_indices] = new_solutions[apply_indices] 185 | self.scores[apply_indices] = new_scores[apply_indices] 186 | 187 | # Increase rate and reduce loudness. 188 | self.loudness[apply_indices] *= self.alpha 189 | self.rates[apply_indices] = self.r_0 * (1 - math.exp(-self.gamma * (i + 1))) 190 | 191 | self.update_best() 192 | self.eval_num += self.N 193 | i += 1 194 | 195 | # Count stagnation iterations (global best doesn't change much). 196 | if abs(prev_best_score - self.gbest_score) <= self.e: 197 | stag_count += 1 198 | elif stag_count > 0: 199 | stag_count = 0 200 | prev_best_score = self.gbest_score 201 | return self.gbest 202 | -------------------------------------------------------------------------------- /models/single_objective/cuckoo_search.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import math 3 | from collections import namedtuple 4 | from models.swarm_algorithm import SwarmAlgorithm 5 | 6 | 7 | CuckooSearchParams = namedtuple( 8 | 'CuckooSearchParams', 9 | ['xi', 'alpha'] 10 | ) 11 | 12 | 13 | class CuckooSearch(SwarmAlgorithm): 14 | ''' 15 | Cuckoo Search algorithm. 16 | 17 | Parameters 18 | ---------- 19 | D : int 20 | Search space dimension. 21 | N : int 22 | Population size. 23 | fit_func : callable 24 | Fitness (objective) function. 25 | params : CuckooSearchParams 26 | Model behavioral parameters. 27 | bounds : ndarray 28 | A 2 by D matrix containing lower and upper bounds of the search space 29 | for each dimension. 30 | seed : int, optional, default=None 31 | Random generator seed. 32 | max_iter : int, optional, default=100 33 | Maximum number of iterations (generations). 34 | stag_iter : int, optional, default=100 35 | Specifies the allowed number of iterations without solution improvement 36 | by equal or more than a given tolerance e. If the number is exceeded, 37 | the optimization process stagnation occurs and the algorithm stops. 38 | e : float, optional, default=1e-5 39 | Tolerance. 40 | 41 | Attributes 42 | ---------- 43 | xi : float 44 | Fraction of nests to abandon. 45 | alpha : float 46 | Step size for Levy flights. 47 | particles : ndarray 48 | An N by D array representing the swarm of N particles. 49 | scores : ndarray 50 | An array of size N representing the value of the fitness function 51 | for each particle. 52 | gbest : ndarray 53 | A D-dimensional vector representing the position of the current 54 | global best particle. 55 | gbest_score : float 56 | The value of the fitness function for the current global best particle. 57 | eval_num : int 58 | The number of fitness function evaluations. 59 | ''' 60 | def __init__(self, D, N, fit_func, params, bounds, seed=None, max_iter=100, 61 | stag_iter=100, e=0.00001): 62 | super().__init__(D, N, fit_func, params, bounds, seed, max_iter, 63 | stag_iter, e) 64 | 65 | # Levy flight's coefficient calculation. 66 | self.beta = 1.5 67 | self.sigma = math.gamma(1 + self.beta) 68 | self.sigma *= math.sin(math.pi * self.beta / 2) 69 | # math.gamma((1 + self.beta) / 2) * self.beta * 2 ** ((self.beta - 1) / 2) 70 | denominator = math.gamma((1 + self.beta) / 2) 71 | denominator *= self.beta * 2 ** ((self.beta - 1) / 2) 72 | self.sigma /= denominator 73 | self.sigma **= (1 / self.beta) 74 | 75 | def set_params(self, new_params): 76 | ''' 77 | Initialize the algorithm with a strategy (vector of parameters). 78 | 79 | Parameters 80 | ---------- 81 | new_params : CuckooSearchParams 82 | 83 | Returns 84 | ------- 85 | No value. 86 | ''' 87 | self.xi = new_params.xi 88 | self.alpha = new_params.alpha 89 | 90 | def optimize(self): 91 | ''' 92 | Main loop of the algorithm. 93 | 94 | Parameters 95 | ---------- 96 | No parameters. 97 | 98 | Returns 99 | ------- 100 | ndarray 101 | The coordinates of the global best particle at the end of 102 | the optimization process. 103 | ''' 104 | i = 0 105 | # Initialize stagnating iterations counter. 106 | stag_count = 0 107 | prev_best_score = self.gbest_score 108 | 109 | # MAIN LOOP 110 | while i < self.max_iter and stag_count < self.stag_iter: 111 | new_nests = self.__get_new_solutions() 112 | self.__eval_solutions(new_nests) 113 | new_nests = self.__empty_nests() 114 | self.__eval_solutions(new_nests) 115 | self.update_best() 116 | self.eval_num += 2 * self.N 117 | i += 1 118 | 119 | # Count stagnation iterations (global best doesn't change much). 120 | if abs(prev_best_score - self.gbest_score) <= self.e: 121 | stag_count += 1 122 | elif stag_count > 0: 123 | stag_count = 0 124 | prev_best_score = self.gbest_score 125 | return self.gbest 126 | 127 | def __eval_solutions(self, new_nests): 128 | ''' 129 | Computes the value of the fitness-function for the new nests and 130 | updates the swarm scores if necessary. 131 | 132 | Parameters 133 | ---------- 134 | new_nests : ndarray 135 | An array of D-dimensional vectors, which represent nests' 136 | positions. 137 | 138 | Returns 139 | ------- 140 | No value. 141 | ''' 142 | new_scores = np.zeros(self.N) 143 | for i in range(self.N): 144 | new_scores[i] = self.fit_func(new_nests[i]) 145 | upd_indexes = new_scores < self.scores 146 | self.scores[upd_indexes] = new_scores[upd_indexes] 147 | self.particles[upd_indexes] = new_nests[upd_indexes] 148 | 149 | def __get_new_solutions(self): 150 | ''' 151 | Gets new solutions by performing Levy flights. 152 | 153 | Parameters 154 | ---------- 155 | No parameters. 156 | 157 | Returns 158 | ------- 159 | ndarray 160 | An array of D-dimensional vectors, which represent new nests' 161 | positions. 162 | ''' 163 | new_solutions = np.copy(self.particles) 164 | u = np.random.normal(size=(self.N, self.D)) * self.sigma 165 | v = np.random.normal(size=(self.N, self.D)) 166 | step = u / np.abs(v) ** (1 / self.beta) 167 | stepsize = self.alpha * step * (new_solutions - self.gbest) 168 | new_solutions += np.random.normal(size=(self.N, self.D)) * stepsize 169 | self.simplebounds(new_solutions) 170 | return new_solutions 171 | 172 | def __empty_nests(self): 173 | ''' 174 | Replaces a fraction of nests (determined by xi) by generating 175 | new solutions. 176 | 177 | Parameters 178 | ---------- 179 | No parameters. 180 | 181 | Returns 182 | ------- 183 | ndarray 184 | An array of D-dimensional vectors, which represent new nests' 185 | positions. 186 | ''' 187 | # Decision vector (is discovered). 188 | decisions = np.random.uniform(size=self.N) > self.xi 189 | 190 | stepsize = self.particles[np.random.permutation(self.N)] 191 | stepsize -= self.particles[np.random.permutation(self.N)] 192 | stepsize *= np.random.uniform(0, 1, self.D) 193 | 194 | new_nests = self.particles + stepsize * decisions[:, np.newaxis] 195 | self.simplebounds(new_nests) 196 | return new_nests 197 | -------------------------------------------------------------------------------- /models/single_objective/firefly_algorithm.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from collections import namedtuple 3 | from models.swarm_algorithm import SwarmAlgorithm 4 | 5 | 6 | FireflyAlgorithmParams = namedtuple( 7 | 'FireflyAlgorithmParams', 8 | ['beta_0', 'alpha_0', 'alpha_inf', 'gamma', 'lambd'] 9 | ) 10 | 11 | 12 | class FireflyAlgorithm(SwarmAlgorithm): 13 | ''' 14 | Firefly algorithm. 15 | 16 | Parameters 17 | ---------- 18 | D : int 19 | Search space dimension. 20 | N : int 21 | Population size. 22 | fit_func : callable 23 | Fitness (objective) function. 24 | params : FireflyAlgorithmParams 25 | Model behavioral parameters. 26 | bounds : ndarray 27 | A 2 by D matrix containing lower and upper bounds of the search space 28 | for each dimension. 29 | seed : int, optional, default=None 30 | Random generator seed. 31 | max_iter : int, optional, default=100 32 | Maximum number of iterations (generations). 33 | stag_iter : int, optional, default=100 34 | Specifies the allowed number of iterations without solution improvement 35 | by equal or more than a given tolerance. If the number is exceeded, 36 | the optimization process stagnations occurs and the algorithm stops. 37 | e : float, optional, default=1e-5 38 | Tolerance. 39 | 40 | Attributes 41 | ---------- 42 | beta_0 : float 43 | Zero distance attractiveness. 44 | alpha_0 : float 45 | Initial value of randomization coefficient. 46 | alpha_inf : float 47 | Final value of randomization coefficient. 48 | alpha : float 49 | Randomization coefficient. 50 | gamma : float 51 | Light absorption coefficient. 52 | lambd : float 53 | Randomization coefficient determining the weight of the third component 54 | of the update rule, which coordinates the movement towards 55 | the global best solution. 56 | particles : ndarray 57 | An N by D array representing the swarm of N particles. 58 | scores : ndarray 59 | An array of size N representing the value of the fitness function 60 | for each particle. 61 | gbest : ndarray 62 | A D-dimensional vector representing the position of the current 63 | global best particle. 64 | gbest_score : float 65 | The value of the fitness function for the current global best particle. 66 | eval_num : int 67 | The number of fitness function evaluations. 68 | ''' 69 | def __init__(self, D, N, fit_func, params, bounds, seed=None, max_iter=100, 70 | stag_iter=100, e=0.0001): 71 | super().__init__(D, N, fit_func, params, bounds, seed, max_iter, 72 | stag_iter, e) 73 | 74 | def reset(self): 75 | ''' 76 | Resets the algorithm state. 77 | 78 | Parameters 79 | ---------- 80 | No parameters. 81 | 82 | Returns 83 | ------- 84 | No value. 85 | ''' 86 | SwarmAlgorithm.reset(self) 87 | self.alpha = self.alpha_0 88 | 89 | def set_params(self, new_params): 90 | ''' 91 | Initialize the algorithm with a strategy (vector of parameters). 92 | 93 | Parameters 94 | ---------- 95 | new_params : FireflyAlgorithmParams 96 | 97 | Returns 98 | ------- 99 | No value. 100 | ''' 101 | self.beta_0 = new_params.beta_0 102 | self.alpha_0 = new_params.alpha_0 103 | self.alpha_inf = new_params.alpha_inf 104 | self.gamma = new_params.gamma 105 | self.lambd = new_params.lambd 106 | 107 | def optimize(self): 108 | ''' 109 | Main loop of the algorithm. 110 | 111 | Parameters 112 | ---------- 113 | No parameters. 114 | 115 | Returns 116 | ------- 117 | ndarray 118 | The coordinates of the global best particle at the end of 119 | the optimization process. 120 | ''' 121 | i = 0 122 | # Initialize stagnating iterations counter. 123 | stag_count = 0 124 | prev_best_score = self.gbest_score 125 | 126 | # MAIN LOOP 127 | while (i < self.max_iter) and (stag_count < self.stag_iter): 128 | self.__reduce_alpha(i) 129 | 130 | # Rank the fireflies. 131 | sorted_indices = np.argsort(self.scores) 132 | self.scores = self.scores[sorted_indices] 133 | self.particles = self.particles[sorted_indices] 134 | self.gbest = np.copy(self.particles[0]) 135 | self.gbest_score = self.scores[0] 136 | self.__move_all() 137 | 138 | for j in range(self.N): 139 | self.scores[j] = self.fit_func(self.particles[j]) 140 | 141 | self.eval_num += self.N 142 | i += 1 143 | 144 | # Count stagnation iterations (global best doesn't change much). 145 | if abs(prev_best_score - self.gbest_score) <= self.e: 146 | stag_count += 1 147 | elif stag_count > 0: 148 | stag_count = 0 149 | 150 | prev_best_score = self.gbest_score 151 | return self.gbest 152 | 153 | def __move_first_to_second(self, f1, f2): 154 | ''' 155 | Moves the first firefly (first argument) towards the second one (second 156 | argument). 157 | 158 | Parameters 159 | ---------- 160 | f1 : ndarray 161 | A vector representing firefly to move. 162 | f2 : ndarray 163 | A vector representing firefly to move to. 164 | 165 | Returns 166 | ------- 167 | No value. 168 | ''' 169 | # Euclidian distance between two fireflies. 170 | r = np.linalg.norm(f1 - f2) 171 | 172 | # Get the attractiveness of the firefly we want to fly to. 173 | # beta = self.beta_0 * math.exp(-self.gamma * r ** 2) 174 | beta = self.beta_0 / (1 + self.gamma * r ** 2) 175 | 176 | diff = f2 - f1 177 | urand_1 = np.random.randn(self.D) 178 | 179 | # Update coordinates according to the flight formula. 180 | f1 += beta * diff + self.alpha * urand_1 181 | 182 | # Modifications: 183 | # Add an extra term to the flight formula, which uses global best 184 | # solution to improve the efficiency. 185 | urand_2 = np.random.randn(self.D) 186 | f1 += self.lambd * urand_2 * (self.gbest - f1) 187 | 188 | def __move_all(self): 189 | ''' 190 | Updates the positions of all the fireflies in the swarm. 191 | 192 | Parameters 193 | ---------- 194 | No parameters. 195 | 196 | Returns 197 | ------- 198 | No value. 199 | ''' 200 | for i in range(self.N): 201 | for j in range(self.N): 202 | if self.scores[j] < self.scores[i]: 203 | # Move less attractive to more attractive. 204 | self.__move_first_to_second( 205 | self.particles[i], 206 | self.particles[j] 207 | ) 208 | self.simplebounds(self.particles) 209 | 210 | def __reduce_alpha(self, iteration): 211 | ''' 212 | Reduces alfa as the number of iterations increases. 213 | Optional modification. 214 | 215 | Parameters 216 | ---------- 217 | No parameters. 218 | 219 | Returns 220 | ------- 221 | No value. 222 | ''' 223 | alpha_range = self.alpha_0 - self.alpha_inf 224 | self.alpha = self.alpha_inf + alpha_range * np.exp(-iteration) 225 | -------------------------------------------------------------------------------- /models/single_objective/gravitational_search.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import math 3 | from collections import namedtuple 4 | from models.swarm_algorithm import SwarmAlgorithm 5 | 6 | 7 | GravitationalSearchParams = namedtuple( 8 | 'GravitationalSearchParams', 9 | ['G0', 'alpha'] 10 | ) 11 | 12 | 13 | class GravitationalSearch(SwarmAlgorithm): 14 | ''' 15 | Gravitational Search algorithm. 16 | 17 | Parameters 18 | ---------- 19 | D : int 20 | Search space dimension. 21 | N : int 22 | Population size. 23 | fit_func : callable 24 | Fitness (objective) function. 25 | params : GravitationalSearchParams 26 | Model behavioral parameters. 27 | bounds : ndarray 28 | A 2 by D matrix containing lower and upper bounds of the search space 29 | for each dimension. 30 | seed : int, optional, default=None 31 | Random generator seed. 32 | max_iter : int, optional, default=100 33 | Maximum number of iterations (generations). 34 | stag_iter : int, optional, default=100 35 | Specifies the allowed number of iterations without solution improvement 36 | by equal or more than a given tolerance. If the number is exceeded, 37 | the optimization process stagnations occurs and the algorithm stops. 38 | e : float, optional, default=1e-5 39 | Tolerance. 40 | 41 | Attributes 42 | ---------- 43 | G0 : float 44 | The initial value of gravitational constant. 45 | alpha : float 46 | Reduction regulation parameter. 47 | particles : ndarray 48 | An N by D array representing the swarm of N particles. 49 | scores : ndarray 50 | An array of size N representing the value of the fitness function 51 | for each particle. 52 | gbest : ndarray 53 | A D-dimensional vector representing the position of the current 54 | global best particle. 55 | gbest_score : float 56 | The value of the fitness function for the current global best particle. 57 | eval_num : int 58 | The number of fitness function evaluations. 59 | ''' 60 | def __init__(self, D, N, fit_func, params, bounds, seed=None, max_iter=100, 61 | stag_iter=100, e=0.00001): 62 | super().__init__(D, N, fit_func, params, bounds, seed, max_iter, 63 | stag_iter, e) 64 | 65 | def set_population(self, new_population): 66 | ''' 67 | Sets a population with a pre-generated one. 68 | 69 | Parameters 70 | ---------- 71 | new_population: array_like 72 | A matrix with dimensions N by D, which represents the coordinates 73 | of each particle. 74 | 75 | Returns 76 | ------- 77 | No value. 78 | ''' 79 | SwarmAlgorithm.set_population(self, new_population) 80 | self.velocities = np.zeros((self.N, self.D)) 81 | 82 | def set_params(self, new_params): 83 | ''' 84 | Initialize the algorithm with a strategy (vector of parameters). 85 | 86 | Parameters 87 | ---------- 88 | new_params : GravitationalSearchParams 89 | 90 | Returns 91 | ------- 92 | No value. 93 | ''' 94 | self.G0 = new_params.G0 95 | self.alpha = new_params.alpha 96 | 97 | def __get_acceleration(self, M, G, iteration): 98 | ''' 99 | Computes the acceleration for each object. 100 | 101 | Parameters 102 | ---------- 103 | M : ndarray 104 | An array of size N representing object (particles) masses. 105 | G : float 106 | Gravitational constant. 107 | iteration : int 108 | Current iteration of the optimization process. 109 | 110 | Returns 111 | ------- 112 | ndarray 113 | An N by D matrix, which represents an array of acceleration vectors 114 | for each object. 115 | ''' 116 | final_per = 2 # Drawn from the original paper implementation. 117 | kbest = final_per + (1 - iteration / self.max_iter) * (100 - final_per) 118 | kbest = math.trunc(self.N * kbest / 100) 119 | M_sorted_i = np.argsort(-M) 120 | E = np.zeros((self.N, self.D)) 121 | 122 | for i in range(self.N): 123 | for ii in range(kbest): 124 | j = M_sorted_i[ii] 125 | if j != i: 126 | R = np.linalg.norm(self.particles[i] - self.particles[j]) 127 | vec_dist = self.particles[j] - self.particles[i] 128 | E[i] += np.random.uniform(size=self.D) * M[j] * vec_dist / (R + 0.001) 129 | return E * G 130 | 131 | def __move_all(self, a): 132 | ''' 133 | Updates the positions of all the particles in the swarm in-place. 134 | 135 | Parameters 136 | ---------- 137 | a : ndarray 138 | An N by D matrix, which represents an array of acceleration vectors 139 | for each object. 140 | 141 | Returns 142 | ------- 143 | No value. 144 | ''' 145 | self.velocities = np.random.uniform(size=(self.N, self.D)) * self.velocities + a 146 | self.particles += self.velocities 147 | self.simplebounds(self.particles) 148 | for i in range(self.N): 149 | self.scores[i] = self.fit_func(self.particles[i]) 150 | if self.scores[i] < self.gbest_score: 151 | self.gbest_score = self.scores[i] 152 | self.gbest = np.copy(self.particles[i]) 153 | 154 | def __mass_calc(self): 155 | ''' 156 | Calculates object masses based on the fitness-function values. 157 | 158 | Parameters 159 | ---------- 160 | No parameters. 161 | 162 | Returns 163 | ------- 164 | ndarray 165 | An array of size N containing object masses. 166 | ''' 167 | f_min = np.min(self.scores) 168 | f_max = np.max(self.scores) 169 | 170 | if f_max == f_min: 171 | M = np.ones(self.N) 172 | else: 173 | M = (self.scores - f_max) / (f_min - f_max) 174 | return M / np.sum(M) 175 | 176 | def __g_const_calc(self, iteration): 177 | ''' 178 | Reduces gravitational constant as the iterations' number increases 179 | (makes the search more accurate). 180 | 181 | Parameters 182 | ---------- 183 | iteration : int 184 | Current iteration of the optimization process. 185 | Returns 186 | ------- 187 | float 188 | New value of gravitational constant. 189 | ''' 190 | # return self.G0 * (1 / (self.eval_num + 1)) ** self.alpha 191 | return self.G0 * math.exp(-self.alpha * iteration / self.max_iter) 192 | 193 | def optimize(self): 194 | ''' 195 | Main loop of the algorithm. 196 | 197 | Parameters 198 | ---------- 199 | No parameters. 200 | 201 | Returns 202 | ------- 203 | ndarray 204 | The coordinates of the global best particle at the end of 205 | the optimization process. 206 | ''' 207 | i = 0 208 | while (i < self.max_iter): 209 | M = self.__mass_calc() 210 | G = self.__g_const_calc(i + 1) 211 | a = self.__get_acceleration(M, G, i + 1) 212 | self.__move_all(a) 213 | self.eval_num += self.N 214 | i += 1 215 | return self.gbest 216 | -------------------------------------------------------------------------------- /models/single_objective/harmony_search.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from collections import namedtuple 3 | from models.swarm_algorithm import SwarmAlgorithm 4 | 5 | 6 | HarmonySearchParams = namedtuple( 7 | 'HarmonySearchParams', 8 | ['r_accept', 'r_pa', 'b_range'] 9 | ) 10 | 11 | 12 | class HarmonySearch(SwarmAlgorithm): 13 | ''' 14 | Harmony Search algorithm. 15 | 16 | Parameters 17 | ---------- 18 | D : int 19 | Search space dimension. 20 | N : int 21 | Population size. 22 | fit_func : callable 23 | Fitness (objective) function. 24 | params : PSOParams 25 | Model behavioral parameters. 26 | bounds : ndarray 27 | A 2 by D matrix containing lower and upper bounds of the search space 28 | for each dimension. 29 | seed : int, optional, default=None 30 | Random generator seed. 31 | max_iter : int, optional, default=100 32 | Maximum number of iterations (generations). 33 | stag_iter : int, optional, default=100 34 | Specifies the allowed number of iterations without solution improvement 35 | by equal or more than a given tolerance. If the number is exceeded, 36 | the optimization process stagnations occurs and the algorithm stops. 37 | e : float, optional, default=1e-5 38 | Tolerance. 39 | 40 | Attributes 41 | ---------- 42 | r_accept : float 43 | Memory accepting rate. 44 | r_pa : float 45 | Pitch adjusting rate. 46 | b_range : float 47 | Pitch bandwidth. 48 | particles : ndarray 49 | An N by D array representing the swarm of N particles. 50 | scores : ndarray 51 | An array of size N representing the value of the fitness function 52 | for each particle. 53 | gbest : ndarray 54 | A D-dimensional vector representing the position of the current 55 | global best particle. 56 | gbest_score : float 57 | The value of the fitness function for the current global best particle. 58 | pbest : ndarray 59 | An N by D array representing the best positions found by each particle 60 | individually. 61 | pbest_scores : ndarray 62 | Fitness function values for positions in pbest attribute. 63 | eval_num : int 64 | The number of fitness function evaluations. 65 | ''' 66 | def __init__(self, D, N, fit_func, params, bounds, seed=None, max_iter=1000, 67 | stag_iter=1000, e=0.00001): 68 | super().__init__(D, N, fit_func, params, bounds, seed, max_iter, 69 | stag_iter, e) 70 | 71 | def set_params(self, new_params): 72 | ''' 73 | Initialize the algorithm with a strategy (vector of parameters). 74 | 75 | Parameters 76 | ---------- 77 | new_params : PSOParams 78 | 79 | Returns 80 | ------- 81 | No value. 82 | ''' 83 | self.r_accept = new_params.r_accept 84 | self.r_pa = new_params.r_pa 85 | self.b_range = new_params.b_range 86 | 87 | def __form_new_harmony(self): 88 | ''' 89 | Generates new solutions by adjusting frequencies, randomizing, etc. 90 | 91 | Parameters 92 | ---------- 93 | No parameters. 94 | 95 | Returns 96 | ------- 97 | No value. 98 | ''' 99 | new_harmonies = np.zeros((self.N, self.D)) 100 | decisions = np.random.uniform(size=(self.N, self.D)) > self.r_accept 101 | inv_decisions = ~decisions 102 | 103 | coords_range = self.u_bounds - self.l_bounds 104 | new_coordinates = self.l_bounds + np.random.rand(self.N, self.D) * coords_range 105 | 106 | new_harmonies[decisions] = new_coordinates[decisions] 107 | random_harmonies = self.particles[np.random.choice(self.N, self.N)] 108 | new_harmonies[inv_decisions] = random_harmonies[inv_decisions] 109 | 110 | adjust_probs = np.random.uniform(size=(self.N, self.D)) > self.r_pa 111 | adjust_indices = np.logical_and(decisions, adjust_probs) 112 | adjust_shape = new_harmonies[adjust_indices].shape 113 | adjust_step = self.b_range * np.random.normal(size=adjust_shape) 114 | adjust_step *= np.random.choice([1, -1]) 115 | new_harmonies[adjust_indices] += adjust_step 116 | 117 | for i in range(self.N): 118 | new_score = self.fit_func(new_harmonies[i]) 119 | worst_index = np.argmax(self.scores) 120 | if new_score < self.scores[worst_index]: 121 | self.scores[worst_index] = new_score 122 | self.particles[worst_index] = new_harmonies[i] 123 | 124 | if new_score < self.gbest_score: 125 | self.gbest = np.copy(new_harmonies[i]) 126 | self.gbest_score = new_score 127 | 128 | def optimize(self): 129 | ''' 130 | Main loop of the algorithm. 131 | 132 | Parameters 133 | ---------- 134 | No parameters. 135 | 136 | Returns 137 | ------- 138 | ndarray 139 | The coordinates of the global best particle at the end of 140 | the optimization process. 141 | ''' 142 | i = 0 143 | while (i < self.max_iter): 144 | self.__form_new_harmony() 145 | self.eval_num += self.N 146 | i += 1 147 | return self.gbest 148 | -------------------------------------------------------------------------------- /models/single_objective/pso.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from collections import namedtuple 3 | from models.swarm_algorithm import SwarmAlgorithm 4 | 5 | 6 | PSOParams = namedtuple( 7 | 'PSOParams', 8 | ['w', 'c1', 'c2'] 9 | ) 10 | 11 | 12 | class PSO(SwarmAlgorithm): 13 | ''' 14 | Particle Swarm Optimization algorithm. 15 | 16 | Parameters 17 | ---------- 18 | D : int 19 | Search space dimension. 20 | N : int 21 | Population size. 22 | fit_func : callable 23 | Fitness (objective) function. 24 | params : PSOParams 25 | Model behavioral parameters. 26 | bounds : ndarray 27 | A 2 by D matrix containing lower and upper bounds of the search space 28 | for each dimension. 29 | seed : int, optional, default=None 30 | Random generator seed. 31 | max_iter : int, optional, default=100 32 | Maximum number of iterations (generations). 33 | stag_iter : int, optional, default=100 34 | Specifies the allowed number of iterations without solution improvement 35 | by equal or more than a given tolerance. If the number is exceeded, 36 | the optimization process stagnations occurs and the algorithm stops. 37 | e : float, optional, default=1e-5 38 | Tolerance. 39 | 40 | Attributes 41 | ---------- 42 | w : float 43 | Inertial parameter. 44 | c1 : float 45 | Cognitive parameter. 46 | c2 : float 47 | Social parameter. 48 | particles : ndarray 49 | An N by D array representing the swarm of N particles. 50 | scores : ndarray 51 | An array of size N representing the value of the fitness function 52 | for each particle. 53 | gbest : ndarray 54 | A D-dimensional vector representing the position of the current 55 | global best particle. 56 | gbest_score : float 57 | The value of the fitness function for the current global best particle. 58 | pbest : ndarray 59 | An N by D array representing the best positions found by each particle 60 | individually. 61 | pbest_scores : ndarray 62 | Fitness function values for positions in pbest attribute. 63 | velocities : ndarray 64 | Particles' velocities. 65 | eval_num : int 66 | The number of fitness function evaluations. 67 | ''' 68 | def __init__(self, D, N, fit_func, params, bounds, seed=None, max_iter=100, 69 | stag_iter=100, e=0.00001): 70 | super().__init__(D, N, fit_func, params, bounds, seed, max_iter, 71 | stag_iter, e) 72 | 73 | def set_population(self, new_population): 74 | ''' 75 | Sets a population with a pre-generated one. 76 | 77 | Parameters 78 | ---------- 79 | new_population: array_like 80 | A matrix with dimensions N by D, which represents the coordinates 81 | of each particle. 82 | 83 | Returns 84 | ------- 85 | No value. 86 | ''' 87 | SwarmAlgorithm.set_population(self, new_population) 88 | self.velocities = np.zeros((self.N, self.D)) 89 | 90 | # Compute initial best positions for each particle individually. 91 | self.pbest = np.copy(self.particles) 92 | self.pbest_scores = np.copy(self.scores) 93 | 94 | def set_params(self, new_params): 95 | ''' 96 | Initialize the algorithm with a strategy (vector of parameters). 97 | 98 | Parameters 99 | ---------- 100 | new_params : PSOParams 101 | 102 | Returns 103 | ------- 104 | No value. 105 | ''' 106 | self.w = new_params.w 107 | self.c1 = new_params.c1 108 | self.c2 = new_params.c2 109 | 110 | def __move_all(self): 111 | ''' 112 | Updates the positions of all the particles in the swarm in-place. 113 | 114 | Parameters 115 | ---------- 116 | No parameters. 117 | 118 | Returns 119 | ------- 120 | No value. 121 | ''' 122 | self.velocities = self.w * self.velocities 123 | U1 = np.random.uniform(0, self.c1, (self.N, self.D)) 124 | U2 = np.random.uniform(0, self.c2, (self.N, self.D)) 125 | self.velocities += U1 * (self.pbest - self.particles) 126 | self.velocities += U2 * (self.gbest - self.particles) 127 | self.particles += self.velocities 128 | self.simplebounds(self.particles) 129 | 130 | # Get new objective function values and update best solutions. 131 | for i in range(self.N): 132 | self.scores[i] = self.fit_func(self.particles[i]) 133 | 134 | better_individual = self.scores < self.pbest_scores 135 | self.pbest[better_individual] = np.copy(self.particles[better_individual]) 136 | self.pbest_scores[better_individual] = self.scores[better_individual] 137 | 138 | self.update_best() 139 | 140 | # Main loop of the algorithm. 141 | def optimize(self): 142 | ''' 143 | Main loop of the algorithm. 144 | 145 | Parameters 146 | ---------- 147 | No parameters. 148 | 149 | Returns 150 | ------- 151 | ndarray 152 | The coordinates of the global best particle at the end of 153 | the optimization process. 154 | ''' 155 | i = 0 156 | # Initialize stagnating iterations counter. 157 | stag_count = 0 158 | prev_best_score = self.gbest_score 159 | 160 | # MAIN LOOP 161 | while (i < self.max_iter) and (stag_count < self.stag_iter): 162 | self.__move_all() 163 | self.eval_num += self.N 164 | i += 1 165 | # Count stagnation iterations (global best doesn't change much). 166 | if abs(prev_best_score - self.gbest_score) <= self.e: 167 | stag_count += 1 168 | elif stag_count > 0: 169 | stag_count = 0 170 | prev_best_score = self.gbest_score 171 | return self.gbest 172 | -------------------------------------------------------------------------------- /models/swarm_algorithm.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | import numpy as np 3 | 4 | 5 | class SwarmAlgorithm(ABC): 6 | ''' 7 | A base abstract class for different swarm algorithms. 8 | 9 | Parameters 10 | ---------- 11 | D : int 12 | Search space dimension. 13 | N : int 14 | Population size. 15 | fit_func : callable 16 | Fitness (objective) function or a function returning multiple values 17 | corresponding to different objectives (for multi-objective problems). 18 | params : array_like 19 | Model behavioral parameters. 20 | bounds : ndarray 21 | A 2 by D matrix containing lower and upper bounds of the search space 22 | for each dimension. 23 | seed : int, optional, default=None 24 | Random generator seed. 25 | max_iter : int, optional, default=100 26 | Maximum number of iterations (generations). 27 | stag_iter : int, optional, default=100 28 | Specifies the allowed number of iterations without solution improvement 29 | by equal or more than a given tolerance. If the number is exceeded, 30 | the optimization process stagnations occurs and the algorithm stops. 31 | e : float, optional, default=1e-5 32 | Tolerance. 33 | 34 | Attributes 35 | ---------- 36 | particles : ndarray 37 | An N by D array representing the swarm of N particles. 38 | scores : ndarray 39 | An array of size N representing the value of the fitness function 40 | for each particle. 41 | gbest : ndarray 42 | The D-dimensional vector representing the position of the current 43 | global best particle. 44 | gbest_score : float 45 | The value of the fitness function for the current global best particle. 46 | eval_num : int 47 | The number of fitness function evaluations. 48 | ''' 49 | def __init__(self, D, N, fit_func, params, bounds, seed=None, max_iter=100, 50 | stag_iter=100, e=1e-5): 51 | self.D = D 52 | self.N = N 53 | 54 | # Initialize problem parameters. 55 | self.fit_func = fit_func 56 | self.l_bounds = bounds[0] 57 | self.u_bounds = bounds[1] 58 | 59 | # Behavioural parameters' initialization. 60 | self.set_params(params) 61 | 62 | # Initializing the Numpy random numbers generator to reproduce results 63 | # of the optimization processes. 64 | self.seed = seed 65 | 66 | # Stopping criteria. 67 | self.max_iter = max_iter 68 | self.stag_iter = stag_iter 69 | self.e = e 70 | 71 | self.reset() 72 | 73 | @abstractmethod 74 | def set_params(self, new_params): 75 | ''' 76 | Initialize the algorithm with a strategy (vector of parameters). 77 | 78 | Parameters 79 | ---------- 80 | new_params : array_like 81 | 82 | Returns 83 | ------- 84 | No value. 85 | 86 | ''' 87 | pass 88 | 89 | def reset(self): 90 | ''' 91 | Resets the algorithm state. 92 | 93 | Parameters 94 | ---------- 95 | No parameters. 96 | 97 | Returns 98 | ------- 99 | No value. 100 | ''' 101 | if self.seed is not None: 102 | np.random.seed(self.seed) 103 | 104 | # Generate initial population and particles' velocities. 105 | self.set_population([self.generate_particle() 106 | for _ in range(self.N)]) 107 | 108 | def generate_particle(self): 109 | ''' 110 | Generates a swarm particle within bounds. 111 | 112 | Parameters 113 | ---------- 114 | No parameters. 115 | 116 | Returns 117 | ------- 118 | ndarray 119 | A vector of size D representing particle's coordinates. 120 | ''' 121 | coords_range = self.u_bounds - self.l_bounds 122 | return self.l_bounds + np.random.uniform(size=self.D) * coords_range 123 | 124 | def set_population(self, new_population): 125 | ''' 126 | Sets a population with a pre-generated one. 127 | 128 | Parameters 129 | ---------- 130 | new_population: array_like 131 | A matrix with dimensions N by D, which represents the coordinates 132 | of each particle. 133 | 134 | Returns 135 | ------- 136 | No value. 137 | ''' 138 | self.eval_num = self.N 139 | 140 | self.N = len(new_population) 141 | self.particles = np.copy(new_population) 142 | self.scores = np.array([self.fit_func(p) for p in self.particles]) 143 | 144 | # Initializing current best. 145 | gbest_index = np.ndarray.argmin(self.scores) 146 | self.gbest = np.copy(self.particles[gbest_index]) 147 | self.gbest_score = self.scores[gbest_index] 148 | 149 | @abstractmethod 150 | def optimize(self): 151 | ''' 152 | Main loop of the algorithm. 153 | 154 | Parameters 155 | ---------- 156 | No parameters. 157 | 158 | Returns 159 | ------- 160 | ndarray 161 | The coordinates of the global best particle at the end of 162 | the optimization process. 163 | ''' 164 | pass 165 | 166 | def update_best(self): 167 | ''' 168 | Updates global best particle if needed. 169 | 170 | Parameters 171 | ---------- 172 | No parameters. 173 | 174 | Returns 175 | ------- 176 | No value. 177 | ''' 178 | current_best_index = np.argmin(self.scores) 179 | current_best = self.particles[current_best_index] 180 | current_best_score = self.scores[current_best_index] 181 | 182 | if current_best_score < self.gbest_score: 183 | self.gbest = np.copy(current_best) 184 | self.gbest_score = current_best_score 185 | 186 | def simplebounds(self, coords): 187 | ''' 188 | Simple constraint rule for particles' positions 189 | (in-place coordinate modification). 190 | 191 | Parameters 192 | ---------- 193 | coords: ndarray 194 | An array of particles to apply the rule to. 195 | 196 | Returns 197 | ------- 198 | No value. 199 | ''' 200 | l_bounds_tiled = np.tile(self.l_bounds, [coords.shape[0], 1]) 201 | u_bounds_tiled = np.tile(self.u_bounds, [coords.shape[0], 1]) 202 | lower_bound_indexes = coords < self.l_bounds 203 | upper_bound_indexes = coords > self.u_bounds 204 | coords[lower_bound_indexes] = l_bounds_tiled[lower_bound_indexes] 205 | coords[upper_bound_indexes] = u_bounds_tiled[upper_bound_indexes] 206 | 207 | def info(self): 208 | ''' 209 | Returns basic information about the algorithm state in a 210 | human-readable representation. 211 | 212 | Parameters 213 | ---------- 214 | No parameters. 215 | 216 | Returns 217 | ------- 218 | str 219 | Information about current best position, score and 220 | current number of fitness-function evaluations. 221 | ''' 222 | info = f'Algorithm: {type(self).__name__}\n' 223 | info += f'Best position: {self.gbest}\n' 224 | info += f'Best score: {self.gbest_score}\n' 225 | info += f'Fitness function evaluatiions number: {self.eval_num}' 226 | 227 | return info 228 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cycler==0.10.0 2 | kiwisolver==1.2.0 3 | matplotlib==3.3.0 4 | numpy==1.22.0 5 | Pillow==9.0.1 6 | pyparsing==2.4.7 7 | python-dateutil==2.8.1 8 | six==1.15.0 9 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OleksandrKlanovets/swarm_algorithms/62355a29570a6978943e243538d25365dc3b6cdd/utils/__init__.py -------------------------------------------------------------------------------- /utils/optimization_visualization.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from matplotlib.animation import FuncAnimation 4 | 5 | 6 | def plot_function(func, l_bound, u_bound, step): 7 | ''' 8 | Plots function colormap. 9 | 10 | Parameters 11 | ---------- 12 | func : callable 13 | Function to plot. 14 | l_bound : float 15 | Lower bound for X and Y axes. 16 | u_bound : float 17 | Upper bound for X and Y axes. 18 | step : float 19 | Spacing between points, which is used to calculate 20 | fitness-function value for each particle. 21 | 22 | Returns 23 | ------- 24 | No value. 25 | ''' 26 | # Create a grid for displaying the search space. 27 | X = np.arange(l_bound, u_bound, step) 28 | Y = np.copy(X) 29 | 30 | # Get the values of the target function at cartesian_square(X, Y). 31 | Z = [] 32 | for i in range(len(Y)): 33 | temp = [] 34 | for j in range(len(X)): 35 | temp.append(func(np.array([X[j], Y[i]]))) 36 | Z.append(temp) 37 | 38 | # Plot the target function colormap. 39 | plt.contourf(X, Y, Z, 100) 40 | 41 | 42 | def plot_particles(model): 43 | ''' 44 | Plots the particles at their current positions. 45 | 46 | Parameters 47 | ---------- 48 | model : SwarmAlgorithm 49 | An initialized model used to perform optimization. 50 | 51 | Returns 52 | ------- 53 | plotted_particles : matplotlib.lines.Line2D 54 | Plotted swarm without the global best. 55 | gbest : matplotlib.lines.Line2D 56 | Plotted global best particle. 57 | ''' 58 | indices = np.arange(model.N) 59 | gbest_index = np.argmin(model.scores) 60 | 61 | X = model.particles[indices != gbest_index, 0] 62 | Y = model.particles[indices != gbest_index, 1] 63 | 64 | X_gbest = model.particles[gbest_index, 0] 65 | Y_gbest = model.particles[gbest_index, 1] 66 | 67 | NON_GBEST_SIZE = 8 68 | GBEST_SIZE = 12 69 | plotted_particles, = plt.plot(X, Y, marker='X', color='r', 70 | linewidth=0, markersize=NON_GBEST_SIZE) 71 | gbest, = plt.plot(X_gbest, Y_gbest, marker='*', color='y', 72 | linewidth=0, markersize=GBEST_SIZE) 73 | return plotted_particles, gbest 74 | 75 | 76 | def animate(i, model, plotted_particles): 77 | ''' 78 | Refreshes the particles. 79 | 80 | Parameters 81 | ---------- 82 | i : int 83 | 84 | model : SwarmAlgorithm 85 | An initialized model used to perform optimization. 86 | plotted_particles : matplotlib.lines.Line2D 87 | Plotted particles to update. 88 | Returns 89 | ------- 90 | plotted_particles : matplotlib.lines.Line2D 91 | Updated plotted particles. 92 | ''' 93 | if i > 1: 94 | model.optimize() 95 | 96 | indices = np.arange(model.N) 97 | gbest_index = np.argmin(model.scores) 98 | gbest = model.particles[gbest_index] 99 | non_gbest = model.particles[indices != gbest_index] 100 | 101 | # Set new positions for global best and the rest of the particles. 102 | plotted_particles[0].set_data(non_gbest[:, 0], non_gbest[:, 1]) 103 | plotted_particles[1].set_data(gbest[0], gbest[1]) 104 | return plotted_particles, 105 | 106 | 107 | def visualize_optimization(model, frames_num=100, iteration_duration=200): 108 | ''' 109 | Visualizes the optimization process for single-objective problems 110 | with 2 parameters. 111 | 112 | Parameters 113 | ---------- 114 | model : SwarmAlgorithm 115 | An initialized single-objective model used to perform optimization. 116 | frames_num : int, optional, default=100 117 | Number of frames of the animation (number of generations to visualize). 118 | i_duration : int, optional, default=200 119 | Interval/frame duration in milliseconds. 120 | 121 | Returns 122 | ------- 123 | No value. 124 | ''' 125 | model.max_iter = 1 126 | 127 | # Create a figure to visualize on. 128 | fig = plt.figure() 129 | 130 | # Define bounds. 131 | l_bound = np.min(model.l_bounds) 132 | u_bound = np.max(model.u_bounds) 133 | 134 | FRACTION_DEGREE = 100 135 | step = (u_bound - l_bound) / FRACTION_DEGREE 136 | 137 | model_name = type(model).__name__ 138 | fit_func_name = model.fit_func.__name__.capitalize() 139 | 140 | plot_function(model.fit_func, l_bound - step, u_bound + step, step) 141 | 142 | # Plot legend. 143 | plt.xlabel('$X_1$') 144 | plt.ylabel('$X_2$') 145 | plt.title(model_name + ' visualization for ' + fit_func_name) 146 | plt.colorbar() 147 | 148 | # Plot particles at their initial position. 149 | plotted_particles = plot_particles(model) 150 | 151 | animation = FuncAnimation( 152 | fig, 153 | animate, 154 | frames=frames_num, 155 | interval=iteration_duration, 156 | fargs=(model, plotted_particles), 157 | repeat=False 158 | ) 159 | return animation 160 | -------------------------------------------------------------------------------- /utils/test_functions.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import math 3 | 4 | 5 | def sphere(X): 6 | X = np.asarray_chkfinite(X) 7 | return np.sum(X ** 2) 8 | 9 | 10 | def michalewicz(X, m=5): 11 | X = np.asarray_chkfinite(X) 12 | d = len(X) 13 | i = np.arange(1, d + 1) 14 | return -np.sum(np.sin(X) * np.sin(i * X ** 2 / np.pi) ** (2 * m)) 15 | 16 | 17 | def easom(X): 18 | X = np.asarray_chkfinite(X) 19 | return -math.cos(X[0]) * math.cos(X[1]) * math.exp(-(X[0] - math.pi) ** 2 - (X[1] - math.pi) ** 2) 20 | 21 | 22 | def shubert(X): 23 | X = np.asarray_chkfinite(X) 24 | i = np.arange(1, 6) 25 | return np.sum(i * np.cos((i + 1) * X[0] + i)) * np.sum(i * np.cos((i + 1) * X[1] + i)) 26 | 27 | 28 | def rosenbrock(X): 29 | X = np.asarray_chkfinite(X) 30 | X0 = X[:-1] 31 | X1 = X[1:] 32 | return np.sum((1 - X0) ** 2) + 50 * np.sum((X1 - X0 ** 2) ** 2) 33 | 34 | 35 | def rastrigin(X): 36 | X = np.asarray_chkfinite(X) 37 | d = len(X) 38 | return 5 * d + np.sum(X ** 2 - 5 * np.cos(2 * np.pi * X)) 39 | 40 | 41 | def schwefel(X): 42 | X = np.asarray_chkfinite(X) 43 | d = len(X) 44 | return 418.9829 * d - np.sum(X * np.sin(np.sqrt(np.abs(X)))) 45 | 46 | 47 | def griewank(X): 48 | X = np.asarray_chkfinite(X) 49 | d = len(X) 50 | i = np.arange(1, d + 1) 51 | s = np.sum(X**2) 52 | p = np.prod(np.cos(X / np.sqrt(i))) 53 | return s / 4000 - p + 1 54 | 55 | 56 | def ackley(X, a=20, b=0.2, c=2 * np.pi): 57 | X = np.asarray_chkfinite(X) 58 | d = len(X) 59 | s1 = np.sum(X**2) 60 | s2 = np.sum(np.cos(c * X)) 61 | return -a * np.exp(-b * np.sqrt(s1 / d)) - np.exp(s2 / d) + a + np.exp(1) 62 | 63 | 64 | # Implemented only for d=2 and m=5. 65 | def langermann(X, m=5, c=(1, 2, 5, 2, 3)): 66 | X = np.asarray_chkfinite(X) 67 | c = np.asarray_chkfinite(c) 68 | A = np.array([[3, 5], [5, 2], [2, 1], [1, 4], [7, 9]]) 69 | res = 0 70 | for i in range(m): 71 | s = np.sum((X - A[i]) ** 2) 72 | res += c[i] * np.exp(-1 / np.pi * s) * np.cos(np.pi * s) 73 | return res 74 | 75 | 76 | def dixonprice(X): 77 | X = np.asarray_chkfinite(X) 78 | d = len(X) 79 | j = np.arange(2, d + 1) 80 | X2 = 2 * X ** 2 81 | return sum(j * (X2[1:] - X[:-1]) ** 2 ) + (X[0] - 1) ** 2 82 | 83 | 84 | def levy(X): 85 | X = np.asarray_chkfinite(X) 86 | z = 1 + (X - 1) / 4 87 | return (np.sin(np.pi * z[0]) ** 2 88 | + np.sum((z[:-1] - 1) ** 2 * (1 + 5 * np.sin(np.pi * z[:-1] + 1 ) ** 2)) 89 | + (z[-1] - 1) ** 2 * (1 + np.sin(2 * np.pi * z[-1]) ** 2)) 90 | 91 | 92 | def perm(X, b=.5): 93 | X = np.asarray_chkfinite(X) 94 | d = len(X) 95 | j = np.arange(1., d + 1) 96 | xbyj = np.fabs(X) / j 97 | return np.mean([np.mean( (j ** k + b) * (xbyj ** k - 1)) ** 2 for k in j / d ]) 98 | --------------------------------------------------------------------------------