multi.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. """Multi-objetive classes algorithm
  2. """
  3. # main imports
  4. import logging
  5. import math
  6. import numpy as np
  7. import sys
  8. from macop.utils.progress import macop_text, macop_line, macop_progress
  9. # module imports
  10. from macop.algorithms.base import Algorithm
  11. from macop.evaluators.discrete.multi import WeightedSum
  12. class MOEAD(Algorithm):
  13. """Multi-Ojective Evolutionary Algorithm with Scalar Decomposition
  14. Attributes:
  15. mu: {int} -- number of sub problems
  16. T: {[float]} -- number of neightbors for each sub problem
  17. nObjectives: {int} -- number of objectives (based of number evaluator)
  18. initializer: {function} -- basic function strategy to initialize solution
  19. evaluator: {[function]} -- list of basic function in order to obtained fitness (multiple objectives)
  20. operators: {[Operator]} -- list of operator to use when launching algorithm
  21. policy: {Policy} -- Policy class implementation strategy to select operators
  22. validator: {function} -- basic function to check if solution is valid or not under some constraints
  23. maximise: {bool} -- specify kind of optimisation problem
  24. verbose: {bool} -- verbose or not information about the algorithm
  25. population: [{Solution}] -- population of solution, one for each sub problem
  26. pfPop: [{Solution}] -- pareto front population
  27. weights: [[{float}]] -- random weights used for custom mu sub problems
  28. callbacks: {[Callback]} -- list of Callback class implementation to do some instructions every number of evaluations and `load` when initializing algorithm
  29. >>> import random
  30. >>> # operators import
  31. >>> from macop.operators.discrete.crossovers import SimpleCrossover
  32. >>> from macop.operators.discrete.mutators import SimpleMutation
  33. >>> # policy import
  34. >>> from macop.policies.classicals import RandomPolicy
  35. >>> # solution and algorithm
  36. >>> from macop.solutions.discrete import BinarySolution
  37. >>> from macop.algorithms.multi import MOEAD
  38. >>> # evaluator import
  39. >>> from macop.evaluators.discrete.mono import KnapsackEvaluator
  40. >>> # evaluator initialization (worths objects passed into data)
  41. >>> problem_size = 20
  42. >>> worths1 = [ random.randint(0, 20) for i in range(problem_size) ]
  43. >>> evaluator1 = KnapsackEvaluator(data={'worths': worths1})
  44. >>> worths2 = [ random.randint(10, 15) for i in range(problem_size) ]
  45. >>> evaluator2 = KnapsackEvaluator(data={'worths': worths2})
  46. >>> # validator specification (based on weights of each objects)
  47. >>> weights = [ random.randint(5, 30) for i in range(problem_size) ]
  48. >>> validator = lambda solution: True if sum([weights[i] for i, value in enumerate(solution._data) if value == 1]) < 200 else False
  49. >>> # initializer function with lambda function
  50. >>> initializer = lambda x=20: BinarySolution.random(x, validator)
  51. >>> # operators list with crossover and mutation
  52. >>> operators = [SimpleCrossover(), SimpleMutation()]
  53. >>> policy = RandomPolicy(operators)
  54. >>> # MOEAD use multi-objective, hence list of evaluators with mu=100 and T=10
  55. >>> algo = MOEAD(20, 5, initializer, [evaluator1, evaluator2], operators, policy, validator, maximise=True, verbose=False)
  56. >>> # run the algorithm and get the pareto front obtained
  57. >>> pf_solutions = algo.run(100)
  58. >>> # check size of expected pareto
  59. >>> len(pf_solutions)
  60. 33
  61. """
  62. def __init__(self,
  63. mu,
  64. T,
  65. initializer,
  66. evaluator,
  67. operators,
  68. policy,
  69. validator,
  70. maximise=True,
  71. parent=None,
  72. verbose=True):
  73. # redefinition of constructor to well use `initRun` method
  74. self._initializer = initializer
  75. self._evaluator = evaluator
  76. self._operators = operators
  77. self._policy = policy
  78. self._validator = validator
  79. self._callbacks = []
  80. # by default
  81. self._numberOfEvaluations = 0
  82. self._maxEvaluations = 0
  83. self._nObjectives = len(evaluator)
  84. # other parameters
  85. self._parent = parent # parent algorithm if it's sub algorithm
  86. #self.maxEvaluations = 0 # by default
  87. self._maximise = maximise
  88. self._verbose = verbose
  89. # track reference of algo into operator (keep an eye into best solution)
  90. for operator in self._operators:
  91. operator.setAlgo(self)
  92. # by default track reference for policy
  93. self._policy.setAlgo(self)
  94. if mu < T:
  95. raise ValueError('`mu` cannot be less than `T`')
  96. if mu < T:
  97. raise ValueError('`mu` cannot be less than `T`')
  98. self._mu = mu
  99. self._T = T
  100. # initialize neighbors for each sub problem
  101. self.setNeighbors()
  102. weights = []
  103. if self._nObjectives == 2:
  104. for i in range(self._mu):
  105. angle = math.pi / 2 * i / (self._mu - 1)
  106. weights.append([math.cos(angle), math.sin(angle)])
  107. elif self._nObjectives >= 3:
  108. # random weights using uniform
  109. for i in range(self._mu):
  110. w_i = np.random.uniform(0, 1, self._nObjectives)
  111. weights.append(w_i / sum(w_i))
  112. else:
  113. raise ValueError('Unvalid number of objectives')
  114. self._weights = weights
  115. self._subProblems = []
  116. for i in range(self._mu):
  117. # compute weight sum from solution
  118. sub_evaluator = WeightedSum(data={
  119. 'evaluators': evaluator,
  120. 'weights': weights[i]
  121. })
  122. # intialize each sub problem
  123. # use copy of list to keep track for each sub problem
  124. subProblem = MOSubProblem(i,
  125. weights[i],
  126. initializer,
  127. sub_evaluator,
  128. operators.copy(),
  129. policy,
  130. validator,
  131. maximise,
  132. self,
  133. verbose=self._verbose)
  134. self._subProblems.append(subProblem)
  135. self._population = [None for n in range(self._mu)]
  136. self._pfPop = []
  137. # ref point based on number of evaluators
  138. if self._maximise:
  139. self._refPoint = [0 for _ in range(self._nObjectives)]
  140. else:
  141. self._refPoint = [
  142. sys.float_info.max for _ in range(self._nObjectives)
  143. ]
  144. def initRun(self):
  145. """
  146. Method which initialiazes or re-initializes the whole algorithm context specifically for MOEAD
  147. """
  148. # initialization is done during run method
  149. pass
  150. def run(self, evaluations):
  151. """
  152. Run the local search algorithm
  153. Args:
  154. evaluations: {int} -- number of Local search evaluations
  155. Returns:
  156. {Solution} -- best solution found
  157. """
  158. # by default use of mother method to initialize variables
  159. super().run(evaluations)
  160. # enable callback resume for MOEAD
  161. self.resume()
  162. # initialize each sub problem if no backup
  163. for i in range(self._mu):
  164. if self._subProblems[i]._bestSolution is None:
  165. self._subProblems[i].run(1)
  166. self._population[i] = self._subProblems[i]._bestSolution
  167. # if no backup for pf population
  168. if len(self._pfPop) == 0:
  169. for i in range(self._mu):
  170. self._pfPop.append(self._subProblems[i]._bestSolution)
  171. # MOEAD algorithm implementation
  172. while not self.stop():
  173. for i in range(self._mu):
  174. # run 1 iteration into sub problem `i`
  175. self._subProblems[i].run(1)
  176. spBestSolution = self._subProblems[i]._bestSolution
  177. self.updateRefPoint(spBestSolution)
  178. # for each neighbor of current sub problem update solution if better
  179. improvment = False
  180. for j in self._neighbors[i]:
  181. if spBestSolution.fitness(
  182. ) > self._subProblems[j]._bestSolution.fitness():
  183. # create new solution based on current new if better, computes fitness associated to new solution for sub problem
  184. newSolution = spBestSolution.clone()
  185. # evaluate solution for new sub problem and update as best solution
  186. self._subProblems[j].evaluate(newSolution)
  187. self._subProblems[j]._bestSolution = newSolution
  188. # update population solution for this sub problem
  189. self._population[j] = newSolution
  190. improvment = True
  191. # add new solution if improvment is idenfity
  192. if improvment:
  193. self._pfPop.append(spBestSolution)
  194. # update pareto front
  195. self._pfPop = self.paretoFront(self._pfPop)
  196. # add progress here
  197. self.progress()
  198. # stop algorithm if necessary
  199. if self.stop():
  200. break
  201. logging.info(
  202. f"End of {type(self).__name__}, best solution found {self._population}"
  203. )
  204. self.end()
  205. return self._pfPop
  206. def progress(self):
  207. """
  208. Log progress and apply callbacks if necessary
  209. """
  210. if len(self._callbacks) > 0:
  211. for callback in self._callbacks:
  212. callback.run()
  213. macop_progress(self, self.getGlobalEvaluation(),
  214. self.getGlobalMaxEvaluation())
  215. logging.info(
  216. f"-- {type(self).__name__} evaluation {self._numberOfEvaluations} of {self._maxEvaluations} ({((self._numberOfEvaluations) / self._maxEvaluations * 100.):.2f}%)"
  217. )
  218. def setNeighbors(self):
  219. if self._T % 2 == 1:
  220. dmin = -int(self._T / 2)
  221. dmax = int(self._T / 2) + 1
  222. else:
  223. dmin = -int(self._T / 2) + 1
  224. dmax = int(+self._T / 2)
  225. # init neighbord list
  226. self._neighbors = [[] for n in range(self._mu)]
  227. for direction in range(0, -dmin):
  228. for i in range(self._T):
  229. self._neighbors[direction].append(i)
  230. for direction in range(-dmin, self._mu - dmax):
  231. for i in range(direction + dmin, direction + dmax - 1):
  232. self._neighbors[direction].append(i)
  233. for direction in range(self._mu - dmax, self._mu):
  234. for i in range(self._mu - self._T, self._mu):
  235. self._neighbors[direction].append(i)
  236. def updateRefPoint(self, solution):
  237. if self._maximise:
  238. for i in range(len(self._evaluator)):
  239. if solution._scores[i] > self._refPoint[i]:
  240. self._refPoint[i] = solution._scores[i]
  241. else:
  242. for i in range(len(self._evaluator)):
  243. if solution.scores[i] < self._refPoint[i]:
  244. self._refPoint[i] = solution._scores[i]
  245. def paretoFront(self, population):
  246. paFront = []
  247. indexes = []
  248. nObjectives = len(self._evaluator)
  249. nSolutions = len(population)
  250. # find dominated solution
  251. for i in range(nSolutions):
  252. for j in range(nSolutions):
  253. if j in indexes:
  254. continue
  255. # check if solutions are the same
  256. if all([
  257. population[i]._data[k] == population[j]._data[k]
  258. for k in range(len(population[i]._data))
  259. ]):
  260. continue
  261. nDominated = 0
  262. # check number of dominated objectives of current solution by the others solution
  263. for k in range(len(self._evaluator)):
  264. if self._maximise:
  265. if population[i]._scores[k] < population[j]._scores[k]:
  266. nDominated += 1
  267. else:
  268. if population[i]._scores[k] > population[j]._scores[k]:
  269. nDominated += 1
  270. if nDominated == nObjectives:
  271. indexes.append(i)
  272. break
  273. # store the non dominated solution into pareto front
  274. for i in range(nSolutions):
  275. if i not in indexes:
  276. paFront.append(population[i])
  277. return paFront
  278. def end(self):
  279. """Display end message into `run` method
  280. """
  281. macop_text(
  282. self,
  283. f'({type(self).__name__}) Found after {self._numberOfEvaluations} evaluations'
  284. )
  285. for i, solution in enumerate(self._pfPop):
  286. macop_text(self, f' - [{i}] {solution._scores} : {solution}')
  287. macop_line(self)
  288. def information(self):
  289. logging.info("-- Pareto front :")
  290. for i, solution in enumerate(self._pfPop):
  291. logging.info(f"-- {i}] SCORE {solution._scores} - {solution}")
  292. def __str__(self):
  293. return f"{type(self).__name__} using {type(self._population).__name__}"
  294. class MOSubProblem(Algorithm):
  295. """Specific MO sub problem used into MOEAD
  296. Attributes:
  297. index: {int} -- sub problem index
  298. weights: {[float]} -- sub problems objectives weights
  299. initalizer: {function} -- basic function strategy to initialize solution
  300. evaluator: {function} -- basic function in order to obtained fitness (mono or multiple objectives)
  301. operators: {[Operator]} -- list of operator to use when launching algorithm
  302. policy: {Policy} -- Policy class implementation strategy to select operators
  303. validator: {function} -- basic function to check if solution is valid or not under some constraints
  304. maximise: {bool} -- specify kind of optimisation problem
  305. verbose: {bool} -- verbose or not information about the algorithm
  306. currentSolution: {Solution} -- current solution managed for current evaluation
  307. bestSolution: {Solution} -- best solution found so far during running algorithm
  308. callbacks: {[Callback]} -- list of Callback class implementation to do some instructions every number of evaluations and `load` when initializing algorithm
  309. Example:
  310. >>> import random
  311. >>> # operators import
  312. >>> from macop.operators.discrete.crossovers import SimpleCrossover
  313. >>> from macop.operators.discrete.mutators import SimpleMutation
  314. >>> # policy import
  315. >>> from macop.policies.classicals import RandomPolicy
  316. >>> # solution and algorithm
  317. >>> from macop.solutions.discrete import BinarySolution
  318. >>> from macop.algorithms.multi import MOEAD, MOSubProblem
  319. >>> # evaluator import
  320. >>> from macop.evaluators.discrete.mono import KnapsackEvaluator
  321. >>> # evaluator initialization (worths objects passed into data)
  322. >>> problem_size = 20
  323. >>> worths1 = [ random.randint(0, 20) for i in range(problem_size) ]
  324. >>> evaluator1 = KnapsackEvaluator(data={'worths': worths1})
  325. >>> worths2 = [ random.randint(10, 15) for i in range(problem_size) ]
  326. >>> evaluator2 = KnapsackEvaluator(data={'worths': worths2})
  327. >>> # validator specification (based on weights of each objects)
  328. >>> weights = [ random.randint(5, 30) for i in range(problem_size) ]
  329. >>> validator = lambda solution: True if sum([weights[i] for i, value in enumerate(solution._data) if value == 1]) < 200 else False
  330. >>> # initializer function with lambda function
  331. >>> initializer = lambda x=20: BinarySolution.random(x, validator)
  332. >>> # operators list with crossover and mutation
  333. >>> operators = [SimpleCrossover(), SimpleMutation()]
  334. >>> policy = RandomPolicy(operators)
  335. >>> algo = MOEAD(20, 5, initializer, [evaluator1, evaluator2], operators, policy, validator, maximise=True, verbose=False)
  336. >>> # weights of the sub problem
  337. >>> sub_problem_weights = [0.4, 0.6]
  338. >>> sub_evaluator = WeightedSum(data={'evaluators': [evaluator1, evaluator2], 'weights': sub_problem_weights})
  339. >>> # first parameter is the index of the MOSubProblem
  340. >>> subProblem = MOSubProblem(0, sub_problem_weights, initializer, sub_evaluator, operators, policy, validator, maximise=True, parent=algo, verbose=False)
  341. >>> # run the algorithm
  342. >>> solution = subProblem.run(100)
  343. >>> solution._score
  344. 133.0
  345. """
  346. def __init__(self,
  347. index,
  348. weights,
  349. initalizer,
  350. evaluator,
  351. operators,
  352. policy,
  353. validator,
  354. maximise=True,
  355. parent=None,
  356. verbose=True):
  357. super().__init__(initalizer, evaluator, operators, policy, validator,
  358. maximise, parent)
  359. self._index = index
  360. self._weights = weights
  361. self._verbose = verbose
  362. def run(self, evaluations):
  363. """
  364. Run the local search algorithm
  365. Args:
  366. evaluations: {int} -- number of evaluations
  367. Returns:
  368. {Solution} -- best solution found
  369. """
  370. # by default use of mother method to initialize variables
  371. super().run(evaluations)
  372. # initialize solution if necessary
  373. if self._bestSolution is None:
  374. self.initRun()
  375. # new operators list keep track of current sub problem
  376. for op in self._operators:
  377. op.setAlgo(self)
  378. for _ in range(evaluations):
  379. # keep reference of sub problem used
  380. self._policy.setAlgo(self)
  381. # update solution using policy
  382. newSolution = self.update(self._bestSolution)
  383. # if better solution than currently, replace it
  384. if self.isBetter(newSolution):
  385. self._bestSolution = newSolution
  386. # increase number of evaluations
  387. self.increaseEvaluation()
  388. self.progress()
  389. # stop algorithm if necessary
  390. if self.stop():
  391. break
  392. logging.info(
  393. f"---- Current {newSolution} - SCORE {newSolution.fitness()}")
  394. logging.info(
  395. f"End of {type(self).__name__}, best solution found {self._bestSolution}"
  396. )
  397. return self._bestSolution