Source code for mealpy.swarm_based.SSpiderO

#!/usr/bin/env python
# Created by "Thieu" at 12:00, 17/03/2020 ----------%
#       Email: nguyenthieu2102@gmail.com            %
#       Github: https://github.com/thieu1995        %
# --------------------------------------------------%

import numpy as np
from mealpy.optimizer import Optimizer
from mealpy.utils.agent import Agent


[docs]class OriginalSSpiderO(Optimizer): """ The original version of: Social Spider Optimization (SSpiderO) Links: 1. https://www.hindawi.com/journals/mpe/2018/6843923/ Hyper-parameters should fine-tune in approximate range to get faster convergence toward the global optimum: + fp_min (float): Female Percent min, default = 0.65 + fp_max (float): Female Percent max, default = 0.9 Examples ~~~~~~~~ >>> import numpy as np >>> from mealpy import FloatVar, SSpiderO >>> >>> def objective_function(solution): >>> return np.sum(solution**2) >>> >>> problem_dict = { >>> "bounds": FloatVar(n_vars=30, lb=(-10.,) * 30, ub=(10.,) * 30, name="delta"), >>> "minmax": "min", >>> "obj_func": objective_function >>> } >>> >>> model = SSpiderO.OriginalSSpiderO(epoch=1000, pop_size=50, fp_min = 0.65, fp_max = 0.9) >>> g_best = model.solve(problem_dict) >>> print(f"Solution: {g_best.solution}, Fitness: {g_best.target.fitness}") >>> print(f"Solution: {model.g_best.solution}, Fitness: {model.g_best.target.fitness}") References ~~~~~~~~~~ [1] Luque-Chang, A., Cuevas, E., Fausto, F., Zaldivar, D. and Pérez, M., 2018. Social spider optimization algorithm: modifications, applications, and perspectives. Mathematical Problems in Engineering, 2018. """ def __init__(self, epoch: int = 10000, pop_size: int = 100, fp_min: float = 0.65, fp_max: float = 0.9, **kwargs: object) -> None: """ Args: epoch (int): maximum number of iterations, default = 10000 pop_size (int): number of population size, default = 100 fp_min (float): Female Percent min, default = 0.65 fp_max (float): Female Percent max, default = 0.9 """ super().__init__(**kwargs) self.epoch = self.validator.check_int("epoch", epoch, [1, 100000]) self.pop_size = self.validator.check_int("pop_size", pop_size, [5, 10000]) fp_min = self.validator.check_float("fp_min", fp_min, (0., 1.0)) fp_max = self.validator.check_float("fp_max", fp_max, (0., 1.0)) self.fp_min, self.fp_max = min((fp_min, fp_max)), max((fp_min, fp_max)) self.set_parameters(["epoch", "pop_size", "fp_min", "fp_max"])
[docs] def initialization(self): fp_temp = self.fp_min + (self.fp_max - self.fp_min) * self.generator.uniform() # Female Aleatory Percent self.n_f = int(self.pop_size * fp_temp) # number of female self.n_m = self.pop_size - self.n_f # number of male # Probabilities of attraction or repulsion Proper tuning for better results self.p_m = (self.epoch + 1 - np.array(range(1, self.epoch + 1))) / (self.epoch + 1) idx_males = self.generator.choice(range(0, self.pop_size), self.n_m, replace=False) idx_females = set(range(0, self.pop_size)) - set(idx_males) if self.pop is None: self.pop = self.generate_population(self.pop_size) self.pop_males = [self.pop[idx] for idx in idx_males] self.pop_females = [self.pop[idx] for idx in idx_females] self.pop = self.recalculate_weights__(self.pop)
[docs] def generate_empty_agent(self, solution: np.ndarray = None) -> Agent: if solution is None: solution = self.problem.generate_solution(encoded=True) weight = 0.0 return Agent(solution=solution, weight=weight)
[docs] def amend_solution(self, solution: np.ndarray) -> np.ndarray: rd = self.generator.uniform(self.problem.lb, self.problem.ub) condition = np.logical_and(self.problem.lb <= solution, solution <= self.problem.ub) return np.where(condition, solution, rd)
[docs] def move_females__(self, epoch=None): scale_distance = np.sum(self.problem.ub - self.problem.lb) pop = self.pop_females + self.pop_males # Start looking for any stronger vibration for idx in range(0, self.n_f): # Move the females ## Find the position s id_min = None dist_min = 2 ** 16 for jdx in range(0, self.pop_size): if self.pop_females[idx].weight < pop[jdx].weight: dt = np.linalg.norm(pop[jdx].solution - self.pop_females[idx].solution) / scale_distance if dt < dist_min and dt != 0: dist_min = dt id_min = jdx x_s = np.zeros(self.problem.n_dims) vibs = 0 if id_min is not None: vibs = 2 * (pop[id_min].weight * np.exp(-(self.generator.uniform() * dist_min ** 2))) # Vib for the shortest x_s = pop[id_min].solution ## Find the position b dtb = np.linalg.norm(self.g_best.solution - self.pop_females[idx].solution) / scale_distance vibb = 2 * (self.g_best.weight * np.exp(-(self.generator.uniform() * dtb ** 2))) ## Do attraction or repulsion beta = self.generator.uniform(0, 1, self.problem.n_dims) gamma = self.generator.uniform(0, 1, self.problem.n_dims) rd_pos = 2 * self.p_m[epoch-1] * (self.generator.uniform(0, 1, self.problem.n_dims) - 0.5) if self.generator.uniform() >= self.p_m[epoch-1]: # Do an attraction pos_new = self.pop_females[idx].solution + vibs * (x_s - self.pop_females[idx].solution) * beta + \ vibb * (self.g_best.solution - self.pop_females[idx].solution) * gamma + rd_pos else: # Do a repulsion pos_new = self.pop_females[idx].solution - vibs * (x_s - self.pop_females[idx].solution) * beta - \ vibb * (self.g_best.solution - self.pop_females[idx].solution) * gamma + rd_pos pos_new = self.correct_solution(pos_new) self.pop_females[idx].solution = pos_new if self.mode not in self.AVAILABLE_MODES: self.pop_females[idx].target = self.get_target(pos_new) self.pop_females = self.update_target_for_population(self.pop_females)
[docs] def move_males__(self, epoch=None): scale_distance = np.sum(self.problem.ub - self.problem.lb) my_median = np.median([it.weight for it in self.pop_males]) pop = self.pop_females + self.pop_males all_pos = np.array([it.solution for it in pop]) all_wei = np.array([it.weight for it in pop]).reshape((self.pop_size, 1)) total_wei = np.sum(all_wei) if total_wei == 0: mean = np.mean(all_pos, axis=0) else: mean = np.sum(all_wei * all_pos, axis=0) / total_wei for idx in range(0, self.n_m): delta = 2 * self.generator.uniform(0, 1, self.problem.n_dims) - 0.5 rd_pos = 2 * self.p_m[epoch-1] * (self.generator.random(self.problem.n_dims) - 0.5) if self.pop_males[idx].weight >= my_median: # Spider above the median # Start looking for a female with stronger vibration id_min = None dist_min = 99999999 for jdx in range(0, self.n_f): if self.pop_females[jdx].weight > self.pop_males[idx].weight: dt = np.linalg.norm(self.pop_females[jdx].solution - self.pop_males[idx].solution) / scale_distance if dt < dist_min and dt != 0: dist_min = dt id_min = jdx x_s = np.zeros(self.problem.n_dims) vibs = 0 if id_min != None: # Vib for the shortest vibs = 2 * (self.pop_females[id_min].weight * np.exp(-(self.generator.uniform() * dist_min ** 2))) x_s = self.pop_females[id_min].solution pos_new = self.pop_males[idx].solution + vibs * (x_s - self.pop_males[idx].solution) * delta + rd_pos else: # Spider below median, go to weighted mean pos_new = self.pop_males[idx].solution + delta * (mean - self.pop_males[idx].solution) + rd_pos pos_new = self.correct_solution(pos_new) self.pop_males[idx].solution = pos_new if self.mode not in self.AVAILABLE_MODES: self.pop_males[idx].target = self.get_target(pos_new) self.pop_males = self.update_target_for_population(self.pop_males)
### Crossover
[docs] def crossover__(self, mom=None, dad=None, id=0): child1 = np.zeros(self.problem.n_dims) child2 = np.zeros(self.problem.n_dims) if id == 0: # arithmetic recombination r = self.generator.uniform(0.5, 1) # w1 = w2 when r =0.5 child1 = np.multiply(r, mom) + np.multiply((1 - r), dad) child2 = np.multiply(r, dad) + np.multiply((1 - r), mom) elif id == 1: id1 = self.generator.integers(1, int(self.problem.n_dims / 2)) id2 = int(id1 + self.problem.n_dims / 2) child1[:id1] = mom[:id1] child1[id1:id2] = dad[id1:id2] child1[id2:] = mom[id2:] child2[:id1] = dad[:id1] child2[id1:id2] = mom[id1:id2] child2[id2:] = dad[id2:] elif id == 2: temp = int(self.problem.n_dims / 2) child1[:temp] = mom[:temp] child1[temp:] = dad[temp:] child2[:temp] = dad[:temp] child2[temp:] = mom[temp:] return child1, child2
[docs] def mating__(self): # Check whether a spider is good or not (above median) my_median = np.median([it.weight for it in self.pop_males]) pop_males_new = [self.pop_males[idx] for idx in range(self.n_m) if self.pop_males[idx].weight > my_median] # Calculate the radio pop = self.pop_females + self.pop_males all_pos = np.array([agent.solution for agent in pop]) rad = np.max(all_pos, axis=1) - np.min(all_pos, axis=1) r = np.sum(rad) / (2 * self.problem.n_dims) # Start looking if there's a good female near list_child = [] couples = [] for idx in range(0, len(pop_males_new)): for jdx in range(0, self.n_f): dist = np.linalg.norm(pop_males_new[idx].solution - self.pop_females[jdx].solution) if dist < r: couples.append([pop_males_new[idx], self.pop_females[jdx]]) if len(couples) >= 2: n_child = len(couples) for kdx in range(n_child): child1, child2 = self.crossover__(couples[kdx][0].solution, couples[kdx][1].solution, 0) pos1 = self.correct_solution(child1) pos2 = self.correct_solution(child2) agent1 = self.generate_agent(pos1) agent2 = self.generate_agent(pos2) list_child.append(agent1) list_child.append(agent2) list_child += self.generate_population(self.pop_size - len(list_child)) return list_child
[docs] def survive__(self, pop=None, pop_child=None): n_child = len(pop) pop_child = self.get_sorted_and_trimmed_population(pop_child, n_child, self.problem.minmax) for idx in range(0, n_child): if self.compare_target(pop_child[idx].target, pop[idx].target, self.problem.minmax): pop[idx] = pop_child[idx].copy() return pop
[docs] def recalculate_weights__(self, pop=None): fit_total, fit_best, fit_worst = self.get_special_fitness(pop, self.problem.minmax) for idx in range(len(pop)): if fit_best == fit_worst: pop[idx].weight = self.generator.uniform(0.2, 0.8) else: pop[idx].weight = 0.001 + (pop[idx].target.fitness - fit_worst) / (fit_best - fit_worst) return pop
[docs] def evolve(self, epoch): """ The main operations (equations) of algorithm. Inherit from Optimizer class Args: epoch (int): The current iteration """ ### Movement of spiders self.move_females__(epoch) self.move_males__(epoch) # Recalculate weights pop = self.pop_females + self.pop_males pop = self.recalculate_weights__(pop) # Mating Operator pop_child = self.mating__() pop = self.survive__(pop, pop_child) self.pop = self.recalculate_weights__(pop)