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 copy import deepcopy
from mealpy.optimizer import Optimizer


[docs]class BaseSSpiderO(Optimizer): """ The original version of: Social Spider Optimization (SSpiderO) Links: 1. https://www.hindawi.com/journals/mpe/2018/6843923/ Hyper-parameters should fine tuned in approximate range to get faster convergen toward the global optimum: + fp (list): (fp_min, fp_max): Female Percent, default = (0.65, 0.9) Examples ~~~~~~~~ >>> import numpy as np >>> from mealpy.swarm_based.SSpiderO import BaseSSpiderO >>> >>> def fitness_function(solution): >>> return np.sum(solution**2) >>> >>> problem_dict1 = { >>> "fit_func": fitness_function, >>> "lb": [-10, -15, -4, -2, -8], >>> "ub": [10, 15, 12, 8, 20], >>> "minmax": "min", >>> "verbose": True, >>> } >>> >>> epoch = 1000 >>> pop_size = 50 >>> fb = [0.65, 0.9] >>> model = BaseSSpiderO(problem_dict1, epoch, pop_size, fb) >>> best_position, best_fitness = model.solve() >>> print(f"Solution: {best_position}, Fitness: {best_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. """ ID_POS = 0 ID_TAR = 1 ID_WEI = 2 def __init__(self, problem, epoch=10000, pop_size=100, fp=(0.65, 0.9), **kwargs): """ Args: problem (dict): The problem dictionary epoch (int): maximum number of iterations, default = 10000 pop_size (int): number of population size, default = 100 fp (list): (fp_min, fp_max): Female Percent, default = (0.65, 0.9) """ super().__init__(problem, kwargs) self.epoch = epoch self.pop_size = pop_size self.fp = fp
[docs] def create_solution(self): """ To get the position, fitness wrapper, target and obj list + A[self.ID_POS] --> Return: position + A[self.ID_TAR] --> Return: [target, [obj1, obj2, ...]] + A[self.ID_TAR][self.ID_FIT] --> Return: target + A[self.ID_TAR][self.ID_OBJ] --> Return: [obj1, obj2, ...] Returns: list: wrapper of solution with format [position, [target, [obj1, obj2, ...]], weight] """ position = np.random.uniform(self.problem.lb, self.problem.ub) position = self.amend_position(position) fitness = self.get_fitness_position(position) weight = 0.0 return [position, fitness, weight]
[docs] def amend_position(self, position=None): """ If solution out of bound at dimension x, then it will re-arrange to random location in the range of domain Args: position: vector position (location) of the solution. Returns: Amended position """ return np.where(np.logical_and(self.problem.lb <= position, position <= self.problem.ub), position, np.random.uniform(self.problem.lb, self.problem.ub))
[docs] def initialization(self): self.fp = self.fp[0] + (self.fp[1] - self.fp[0]) * np.random.uniform() # Female Aleatory Percent self.n_f = int(self.pop_size * self.fp) # 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) self.pop_males = self.create_population(self.n_m) self.pop_females = self.create_population(self.n_f) pop = deepcopy(self.pop_females) + deepcopy(self.pop_males) self.pop = self._recalculate_weights(pop) _, self.g_best = self.get_global_best_solution(self.pop)
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 i in range(0, self.n_f): # Move the females ## Find the position s id_min = None dist_min = 2 ** 16 for j in range(0, self.pop_size): if self.pop_females[i][self.ID_WEI] < pop[j][self.ID_WEI]: dt = np.linalg.norm(pop[j][self.ID_POS] - self.pop_females[i][self.ID_POS]) / scale_distance if dt < dist_min and dt != 0: dist_min = dt id_min = j x_s = np.zeros(self.problem.n_dims) vibs = 0 if id_min is not None: vibs = 2 * (pop[id_min][self.ID_WEI] * np.exp(-(np.random.uniform() * dist_min ** 2))) # Vib for the shortest x_s = pop[id_min][self.ID_POS] ## Find the position b dtb = np.linalg.norm(self.g_best[self.ID_POS] - self.pop_females[i][self.ID_POS]) / scale_distance vibb = 2 * (self.g_best[self.ID_WEI] * np.exp(-(np.random.uniform() * dtb ** 2))) ## Do attraction or repulsion beta = np.random.uniform(0, 1, self.problem.n_dims) gamma = np.random.uniform(0, 1, self.problem.n_dims) random = 2 * self.p_m[epoch] * (np.random.uniform(0, 1, self.problem.n_dims) - 0.5) if np.random.uniform() >= self.p_m[epoch]: # Do an attraction pos_new = self.pop_females[i][self.ID_POS] + vibs * (x_s - self.pop_females[i][self.ID_POS]) * beta + \ vibb * (self.g_best[self.ID_POS] - self.pop_females[i][self.ID_POS]) * gamma + random else: # Do a repulsion pos_new = self.pop_females[i][self.ID_POS] - vibs * (x_s - self.pop_females[i][self.ID_POS]) * beta - \ vibb * (self.g_best[self.ID_POS] - self.pop_females[i][self.ID_POS]) * gamma + random self.pop_females[i][self.ID_POS] = self.amend_position(pos_new) self.pop_females = self.update_fitness_population(self.pop_females) self.nfe_epoch += self.n_f def _move_males(self, epoch=None): scale_distance = np.sum(self.problem.ub - self.problem.lb) my_median = np.median([it[self.ID_WEI] for it in self.pop_males]) pop = self.pop_females + self.pop_males all_pos = np.array([it[self.ID_POS] for it in pop]) all_wei = np.array([it[self.ID_WEI] 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 i in range(0, self.n_m): delta = 2 * np.random.uniform(0, 1, self.problem.n_dims) - 0.5 random = 2 * self.p_m[epoch] * (np.random.uniform(0, 1, self.problem.n_dims) - 0.5) if self.pop_males[i][self.ID_WEI] >= my_median: # Spider above the median # Start looking for a female with stronger vibration id_min = None dist_min = 99999999 for j in range(0, self.n_f): if self.pop_females[j][self.ID_WEI] > self.pop_males[i][self.ID_WEI]: dt = np.linalg.norm(self.pop_females[j][self.ID_POS] - self.pop_males[i][self.ID_POS]) / scale_distance if dt < dist_min and dt != 0: dist_min = dt id_min = j 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][self.ID_WEI] * np.exp(-(np.random.uniform() * dist_min ** 2))) x_s = self.pop_females[id_min][self.ID_POS] pos_new = self.pop_males[i][self.ID_POS] + vibs * (x_s - self.pop_males[i][self.ID_POS]) * delta + random else: # Spider below median, go to weighted mean pos_new = self.pop_males[i][self.ID_POS] + delta * (mean - self.pop_males[i][self.ID_POS]) + random self.pop_males[i][self.ID_POS] = self.amend_position(pos_new) self.pop_males = self.update_fitness_population(self.pop_males) self.nfe_epoch += self.n_m ### Crossover 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 = np.random.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 = np.random.randint(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 def _mating(self): # Check whether a spider is good or not (above median) my_median = np.median([it[self.ID_WEI] for it in self.pop_males]) pop_males_new = [self.pop_males[i] for i in range(self.n_m) if self.pop_males[i][self.ID_WEI] > my_median] # Calculate the radio pop = self.pop_females + self.pop_males all_pos = np.array([it[self.ID_POS] for it 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 i in range(0, len(pop_males_new)): for j in range(0, self.n_f): dist = np.linalg.norm(pop_males_new[i][self.ID_POS] - self.pop_females[j][self.ID_POS]) if dist < r: couples.append([pop_males_new[i], self.pop_females[j]]) if couples: n_child = len(couples) for k in range(n_child): child1, child2 = self._crossover__(couples[k][0][self.ID_POS], couples[k][1][self.ID_POS], 0) pos1 = self.amend_position(child1) pos2 = self.amend_position(child2) fit1 = self.get_fitness_position(pos1) fit2 = self.get_fitness_position(pos2) list_child.append([pos1, fit1, 0.0]) list_child.append([pos2, fit2, 0.0]) else: list_child = self.create_population(self.pop_size) self.nfe_epoch += len(list_child) return list_child def _survive(self, pop=None, pop_child=None): n_child = len(pop) pop_child = self.get_sorted_strim_population(pop_child, n_child) for i in range(0, n_child): if self.compare_agent(pop_child[i], pop[i]): pop[i] = deepcopy(pop_child[i]) return pop def _recalculate_weights(self, pop=None): fit_total, fit_best, fit_worst = self.get_special_fitness(pop) for i in range(len(pop)): if fit_best == fit_worst: pop[i][self.ID_WEI] = np.random.uniform(0.2, 0.8) else: pop[i][self.ID_WEI] = 0.001 + (pop[i][self.ID_TAR][self.ID_FIT] - 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 """ self.nfe_epoch = 0 ### 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) self.nfe_per_epoch = self.nfe_epoch