ILSSurrogate.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. """Iterated Local Search Algorithm implementation using surrogate as fitness approximation
  2. """
  3. # main imports
  4. import os
  5. import logging
  6. import joblib
  7. # module imports
  8. from macop.algorithms.Algorithm import Algorithm
  9. from .LSSurrogate import LocalSearchSurrogate
  10. from sklearn.linear_model import (LinearRegression, Lasso, Lars, LassoLars,
  11. LassoCV, ElasticNet)
  12. from wsao.sao.problems.nd3dproblem import ND3DProblem
  13. from wsao.sao.surrogates.walsh import WalshSurrogate
  14. from wsao.sao.algos.fitter import FitterAlgo
  15. from wsao.sao.utils.analysis import SamplerAnalysis, FitterAnalysis, OptimizerAnalysis
  16. class ILSSurrogate(Algorithm):
  17. """Iterated Local Search used to avoid local optima and increave EvE (Exploration vs Exploitation) compromise using surrogate
  18. Attributes:
  19. initalizer: {function} -- basic function strategy to initialize solution
  20. evaluator: {function} -- basic function in order to obtained fitness (mono or multiple objectives)
  21. operators: {[Operator]} -- list of operator to use when launching algorithm
  22. policy: {Policy} -- Policy class implementation strategy to select operators
  23. validator: {function} -- basic function to check if solution is valid or not under some constraints
  24. maximise: {bool} -- specify kind of optimization problem
  25. currentSolution: {Solution} -- current solution managed for current evaluation
  26. bestSolution: {Solution} -- best solution found so far during running algorithm
  27. ls_iteration: {int} -- number of evaluation for each local search algorithm
  28. surrogate_file: {str} -- Surrogate model file to load (model trained using https://gitlab.com/florianlprt/wsao)
  29. start_train_surrogate: {int} -- number of evaluation expected before start training and use surrogate
  30. surrogate: {Surrogate} -- Surrogate model instance loaded
  31. ls_train_surrogate: {int} -- Specify if we need to retrain our surrogate model (every Local Search)
  32. solutions_file: {str} -- Path where real evaluated solutions are saved in order to train surrogate again
  33. callbacks: {[Callback]} -- list of Callback class implementation to do some instructions every number of evaluations and `load` when initializing algorithm
  34. """
  35. def __init__(self,
  36. _initalizer,
  37. _evaluator,
  38. _operators,
  39. _policy,
  40. _validator,
  41. _surrogate_file_path,
  42. _start_train_surrogate,
  43. _ls_train_surrogate,
  44. _solutions_file,
  45. _maximise=True,
  46. _parent=None):
  47. # set real evaluator as default
  48. super().__init__(_initalizer, _evaluator, _operators, _policy,
  49. _validator, _maximise, _parent)
  50. self.n_local_search = 0
  51. self.surrogate_file_path = _surrogate_file_path
  52. self.start_train_surrogate = _start_train_surrogate
  53. self.surrogate_evaluator = None
  54. self.ls_train_surrogate = _ls_train_surrogate
  55. self.solutions_file = _solutions_file
  56. def train_surrogate(self):
  57. """etrain if necessary the whole surrogate fitness approximation function
  58. """
  59. # Following https://gitlab.com/florianlprt/wsao, we re-train the model
  60. # ---------------------------------------------------------------------------
  61. # cli_restart.py problem=nd3d,size=30,filename="data/statistics_extended_svdn" \
  62. # model=lasso,alpha=1e-5 \
  63. # surrogate=walsh,order=3 \
  64. # algo=fitter,algo_restarts=10,samplefile=stats_extended.csv \
  65. # sample=1000,step=10 \
  66. # analysis=fitter,logfile=out_fit.csv
  67. problem = ND3DProblem(size=len(self.bestSolution.data)) # problem size based on best solution size (need to improve...)
  68. model = Lasso(alpha=1e-5)
  69. surrogate = WalshSurrogate(order=2, size=problem.size, model=model)
  70. analysis = FitterAnalysis(logfile="train_surrogate.log", problem=problem)
  71. algo = FitterAlgo(problem=problem, surrogate=surrogate, analysis=analysis, seed=problem.seed)
  72. # dynamic number of samples based on dataset real evaluations
  73. nsamples = None
  74. with open(self.solutions_file, 'r') as f:
  75. nsamples = len(f.readlines()) - 1 # avoid header
  76. training_samples = int(0.7 * nsamples) # 70% used for learning part at each iteration
  77. print("Start fitting again the surrogate model")
  78. print(f'Using {training_samples} of {nsamples} samples for train dataset')
  79. for r in range(10):
  80. print("Iteration n°{0}: for fitting surrogate".format(r))
  81. algo.run(samplefile=self.solutions_file, sample=training_samples, step=10)
  82. joblib.dump(algo, self.surrogate_file_path)
  83. def load_surrogate(self):
  84. """Load algorithm with surrogate model and create lambda evaluator function
  85. """
  86. # need to first train surrogate if not exist
  87. if not os.path.exists(self.surrogate_file_path):
  88. self.train_surrogate()
  89. self.surrogate = joblib.load(self.surrogate_file_path)
  90. # update evaluator function
  91. self.surrogate_evaluator = lambda s: self.surrogate.surrogate.predict([s.data])[0]
  92. def add_to_surrogate(self, solution):
  93. # save real evaluated solution into specific file for surrogate
  94. with open(self.solutions_file, 'a') as f:
  95. line = ""
  96. for index, e in enumerate(solution.data):
  97. line += str(e)
  98. if index < len(solution.data) - 1:
  99. line += ","
  100. line += ";"
  101. line += str(solution.score)
  102. f.write(line + "\n")
  103. def run(self, _evaluations, _ls_evaluations=100):
  104. """
  105. Run the iterated local search algorithm using local search (EvE compromise)
  106. Args:
  107. _evaluations: {int} -- number of global evaluations for ILS
  108. _ls_evaluations: {int} -- number of Local search evaluations (default: 100)
  109. Returns:
  110. {Solution} -- best solution found
  111. """
  112. # by default use of mother method to initialize variables
  113. super().run(_evaluations)
  114. # initialize current solution
  115. self.initRun()
  116. # enable resuming for ILS
  117. self.resume()
  118. # count number of surrogate obtained and restart using real evaluations done
  119. nsamples = None
  120. with open(self.solutions_file, 'r') as f:
  121. nsamples = len(f.readlines()) - 1 # avoid header
  122. if self.getGlobalEvaluation() < nsamples:
  123. print(f'Restart using {nsamples} of {self.start_train_surrogate} real evaluations obtained')
  124. self.numberOfEvaluations = nsamples
  125. if self.start_train_surrogate > self.getGlobalEvaluation():
  126. # get `self.start_train_surrogate` number of real evaluations and save it into surrogate dataset file
  127. # using randomly generated solutions (in order to cover seearch space)
  128. while self.start_train_surrogate > self.getGlobalEvaluation():
  129. newSolution = self.initializer()
  130. # evaluate new solution
  131. newSolution.evaluate(self.evaluator)
  132. # add it to surrogate pool
  133. self.add_to_surrogate(newSolution)
  134. self.increaseEvaluation()
  135. # train surrogate on real evaluated solutions file
  136. self.train_surrogate()
  137. self.load_surrogate()
  138. # local search algorithm implementation
  139. while not self.stop():
  140. # set current evaluator based on used or not of surrogate function
  141. current_evaluator = self.surrogate_evaluator if self.start_train_surrogate <= self.getGlobalEvaluation() else self.evaluator
  142. # create new local search instance
  143. # passing global evaluation param from ILS
  144. ls = LocalSearchSurrogate(self.initializer,
  145. current_evaluator,
  146. self.operators,
  147. self.policy,
  148. self.validator,
  149. self.maximise,
  150. _parent=self)
  151. # add same callbacks
  152. for callback in self.callbacks:
  153. ls.addCallback(callback)
  154. # create and search solution from local search
  155. newSolution = ls.run(_ls_evaluations)
  156. # if better solution than currently, replace it (solution saved in training pool, only if surrogate process is in a second process step)
  157. # Update : always add new solution into surrogate pool, not only if solution is better
  158. #if self.isBetter(newSolution) and self.start_train_surrogate < self.getGlobalEvaluation():
  159. if self.start_train_surrogate <= self.getGlobalEvaluation():
  160. # if better solution found from local search, retrained the found solution and test again
  161. # without use of surrogate
  162. fitness_score = self.evaluator(newSolution)
  163. # self.increaseEvaluation() # dot not add evaluation
  164. newSolution.score = fitness_score
  165. # if solution is really better after real evaluation, then we replace
  166. if self.isBetter(newSolution):
  167. self.bestSolution = newSolution
  168. self.add_to_surrogate(newSolution)
  169. self.progress()
  170. # check using specific dynamic criteria based on r^2
  171. r_squared = self.surrogate.analysis.coefficient_of_determination(self.surrogate.surrogate)
  172. training_surrogate_every = int(r_squared * self.ls_train_surrogate)
  173. print(f"=> R^2 of surrogate is of {r_squared}. Retraining model every {training_surrogate_every} LS")
  174. # avoid issue when lauching every each local search
  175. if training_surrogate_every <= 0:
  176. training_surrogate_every = 1
  177. # check if necessary or not to train again surrogate
  178. if self.n_local_search % training_surrogate_every == 0 and self.start_train_surrogate <= self.getGlobalEvaluation():
  179. # train again surrogate on real evaluated solutions file
  180. self.train_surrogate()
  181. # reload new surrogate function
  182. self.load_surrogate()
  183. # increase number of local search done
  184. self.n_local_search += 1
  185. self.information()
  186. logging.info("End of %s, best solution found %s" %
  187. (type(self).__name__, self.bestSolution))
  188. self.end()
  189. return self.bestSolution
  190. def addCallback(self, _callback):
  191. """Add new callback to algorithm specifying usefull parameters
  192. Args:
  193. _callback: {Callback} -- specific Callback instance
  194. """
  195. # specify current main algorithm reference
  196. if self.parent is not None:
  197. _callback.setAlgo(self.parent)
  198. else:
  199. _callback.setAlgo(self)
  200. # set as new
  201. self.callbacks.append(_callback)