MOEAD.py 11 KB

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