MOEAD.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. """Multi-Ojective Evolutionary Algorithm with Scalar Decomposition algorithm
  2. """
  3. # main imports
  4. import pkgutil
  5. import logging
  6. import math
  7. import numpy as np
  8. import sys
  9. from ...utils.color import macop_text, macop_line, macop_progress
  10. from ...utils.modules import load_class
  11. # module imports
  12. from ..Algorithm import Algorithm
  13. from .MOSubProblem import MOSubProblem
  14. def moEvaluator(solution, evaluator, weights):
  15. scores = [eval(solution) for eval in evaluator]
  16. # associate objectives scores to solution
  17. solution._scores = scores
  18. return sum([scores[i] for i, w in enumerate(weights)])
  19. class MOEAD(Algorithm):
  20. """Multi-Ojective Evolutionary Algorithm with Scalar Decomposition
  21. Attributes:
  22. mu: {int} -- number of sub problems
  23. T: {[float]} -- number of neightbors for each sub problem
  24. nObjectives: {int} -- number of objectives (based of number evaluator)
  25. initalizer: {function} -- basic function strategy to initialize solution
  26. evaluator: {[function]} -- list of basic function in order to obtained fitness (multiple objectives)
  27. operators: {[Operator]} -- list of operator to use when launching algorithm
  28. policy: {Policy} -- Policy class implementation strategy to select operators
  29. validator: {function} -- basic function to check if solution is valid or not under some constraints
  30. maximise: {bool} -- specify kind of optimisation problem
  31. population: [{Solution}] -- population of solution, one for each sub problem
  32. pfPop: [{Solution}] -- pareto front population
  33. weights: [[{float}]] -- random weights used for custom mu sub problems
  34. callbacks: {[Callback]} -- list of Callback class implementation to do some instructions every number of evaluations and `load` when initializing algorithm
  35. """
  36. def __init__(self,
  37. mu,
  38. T,
  39. initalizer,
  40. evaluator,
  41. operators,
  42. policy,
  43. validator,
  44. maximise=True,
  45. parent=None):
  46. # redefinition of constructor to well use `initRun` method
  47. self._initializer = initalizer
  48. self._evaluator = evaluator
  49. self._operators = operators
  50. self._policy = policy
  51. self._validator = validator
  52. self._callbacks = []
  53. # by default
  54. self._numberOfEvaluations = 0
  55. self._maxEvaluations = 0
  56. self._nObjectives = len(evaluator)
  57. # other parameters
  58. self._parent = parent # parent algorithm if it's sub algorithm
  59. #self.maxEvaluations = 0 # by default
  60. self._maximise = maximise
  61. # track reference of algo into operator (keep an eye into best solution)
  62. for operator in self._operators:
  63. operator.setAlgo(self)
  64. # by default track reference for policy
  65. self._policy.setAlgo(self)
  66. if mu < T:
  67. raise ValueError('`mu` cannot be less than `T`')
  68. self._mu = mu
  69. self._T = T
  70. # initialize neighbors for each sub problem
  71. self.setNeighbors()
  72. weights = []
  73. if self._nObjectives == 2:
  74. for i in range(self._mu):
  75. angle = math.pi / 2 * i / (self._mu - 1)
  76. weights.append([math.cos(angle), math.sin(angle)])
  77. elif self._nObjectives >= 3:
  78. # random weights using uniform
  79. for i in range(self._mu):
  80. w_i = np.random.uniform(0, 1, self._nObjectives)
  81. weights.append(w_i / sum(w_i))
  82. else:
  83. raise ValueError('Unvalid number of objectives')
  84. self._weights = weights
  85. self._subProblems = []
  86. for i in range(self._mu):
  87. # compute weight sum from solution
  88. sub_evaluator = lambda solution: moEvaluator(
  89. solution, evaluator, weights[i])
  90. # intialize each sub problem
  91. # use copy of list to keep track for each sub problem
  92. subProblem = MOSubProblem(i, weights[i],
  93. initalizer, sub_evaluator,
  94. operators.copy(), policy, validator,
  95. maximise, self)
  96. self._subProblems.append(subProblem)
  97. self._population = [None for n in range(self._mu)]
  98. self._pfPop = []
  99. # ref point based on number of evaluators
  100. if self._maximise:
  101. self._refPoint = [0 for _ in range(self._nObjectives)]
  102. else:
  103. self._refPoint = [
  104. sys.float_info.max for _ in range(self._nObjectives)
  105. ]
  106. def initRun(self):
  107. """
  108. Method which initialiazes or re-initializes the whole algorithm context specifically for MOEAD
  109. """
  110. # initialization is done during run method
  111. pass
  112. def run(self, evaluations):
  113. """
  114. Run the local search algorithm
  115. Args:
  116. evaluations: {int} -- number of Local search evaluations
  117. Returns:
  118. {Solution} -- best solution found
  119. """
  120. # by default use of mother method to initialize variables
  121. super().run(evaluations)
  122. # enable callback resume for MOEAD
  123. self.resume()
  124. # initialize each sub problem if no backup
  125. for i in range(self._mu):
  126. if self._subProblems[i]._bestSolution is None:
  127. self._subProblems[i].run(1)
  128. self._population[i] = self._subProblems[i]._bestSolution
  129. # if no backup for pf population
  130. if len(self._pfPop) == 0:
  131. for i in range(self._mu):
  132. self._pfPop.append(self._subProblems[i]._bestSolution)
  133. # MOEAD algorithm implementation
  134. while not self.stop():
  135. for i in range(self._mu):
  136. # run 1 iteration into sub problem `i`
  137. self._subProblems[i].run(1)
  138. spBestSolution = self._subProblems[i]._bestSolution
  139. self.updateRefPoint(spBestSolution)
  140. # for each neighbor of current sub problem update solution if better
  141. improvment = False
  142. for j in self._neighbors[i]:
  143. if spBestSolution.fitness(
  144. ) > self._subProblems[j]._bestSolution.fitness():
  145. # create new solution based on current new if better, computes fitness associated to new solution for sub problem
  146. class_name = type(spBestSolution).__name__
  147. # dynamically load solution class if unknown
  148. if class_name not in sys.modules:
  149. load_class(class_name, globals())
  150. newSolution = getattr(
  151. globals()['macop.solutions.' + class_name],
  152. class_name)(spBestSolution._data,
  153. len(spBestSolution._data))
  154. # evaluate solution for new sub problem and update as best solution
  155. self._subProblems[j].evaluate(newSolution)
  156. self._subProblems[j]._bestSolution = newSolution
  157. # update population solution for this sub problem
  158. self._population[j] = newSolution
  159. improvment = True
  160. # add new solution if improvment is idenfity
  161. if improvment:
  162. self._pfPop.append(spBestSolution)
  163. # update pareto front
  164. self._pfPop = self.paretoFront(self._pfPop)
  165. # add progress here
  166. self.progress()
  167. # stop algorithm if necessary
  168. if self.stop():
  169. break
  170. logging.info(f"End of {type(self).__name__}, best solution found {self._population}")
  171. self.end()
  172. return self._pfPop
  173. def progress(self):
  174. """
  175. Log progress and apply callbacks if necessary
  176. """
  177. if len(self._callbacks) > 0:
  178. for callback in self._callbacks:
  179. callback.run()
  180. macop_progress(self.getGlobalEvaluation(), self.getGlobalMaxEvaluation())
  181. logging.info(f"-- {type(self).__name__} evaluation {self._numberOfEvaluations} of {self._maxEvaluations} ({((self._numberOfEvaluations) / self._maxEvaluations * 100.):.2f}%)")
  182. def setNeighbors(self):
  183. dmin = dmax = 0
  184. if self._T % 2 == 1:
  185. dmin = -int(self._T / 2)
  186. dmax = int(self._T / 2) + 1
  187. else:
  188. dmin = -int(self._T / 2) + 1
  189. dmax = +self._T / 2
  190. # init neighbord list
  191. self._neighbors = [[] for n in range(self._mu)]
  192. for direction in range(0, -dmin):
  193. for i in range(self._T):
  194. self._neighbors[direction].append(i)
  195. for direction in range(-dmin, self._mu - dmax):
  196. for i in range(direction + dmin, direction + dmax):
  197. self._neighbors[direction].append(i)
  198. for direction in range(self._mu - dmax, self._mu):
  199. for i in range(self._mu - self._T, self._mu):
  200. self._neighbors[direction].append(i)
  201. def updateRefPoint(self, solution):
  202. if self._maximise:
  203. for i in range(len(self._evaluator)):
  204. if solution._scores[i] > self._refPoint[i]:
  205. self._refPoint[i] = solution._scores[i]
  206. else:
  207. for i in range(len(self._evaluator)):
  208. if solution.scores[i] < self._refPoint[i]:
  209. self._refPoint[i] = solution._scores[i]
  210. def paretoFront(self, population):
  211. paFront = []
  212. indexes = []
  213. nObjectives = len(self._evaluator)
  214. nSolutions = len(population)
  215. # find dominated solution
  216. for i in range(nSolutions):
  217. for j in range(nSolutions):
  218. if j in indexes:
  219. continue
  220. # check if solutions are the same
  221. if all([
  222. population[i]._data[k] == population[j]._data[k]
  223. for k in range(len(population[i]._data))
  224. ]):
  225. continue
  226. nDominated = 0
  227. # check number of dominated objectives of current solution by the others solution
  228. for k in range(len(self._evaluator)):
  229. if self._maximise:
  230. if population[i]._scores[k] < population[j]._scores[k]:
  231. nDominated += 1
  232. else:
  233. if population[i]._scores[k] > population[j]._scores[k]:
  234. nDominated += 1
  235. if nDominated == nObjectives:
  236. indexes.append(i)
  237. break
  238. # store the non dominated solution into pareto front
  239. for i in range(nSolutions):
  240. if i not in indexes:
  241. paFront.append(population[i])
  242. return paFront
  243. def end(self):
  244. """Display end message into `run` method
  245. """
  246. print(macop_text(f'({type(self).__name__}) Found after {self._numberOfEvaluations} evaluations'))
  247. for i, solution in enumerate(self._pfPop):
  248. print(f' - [{i}] {solution._scores} : {solution}')
  249. print(macop_line())
  250. def information(self):
  251. logging.info("-- Pareto front :")
  252. for i, solution in enumerate(self._pfPop):
  253. logging.info(f"-- {i}] SCORE {solution._scores} - {solution}")
  254. def __str__(self):
  255. return f"{type(self).__name__} using {type(self._population).__name__}"