Parcourir la source

Add of Zdt continous problem

Jérôme BUISINE il y a 3 ans
Parent
commit
c627fb74d0

+ 3 - 2
docs/source/api.rst

@@ -122,7 +122,7 @@ macop.solutions
 Continuous Optimisation
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Although continuous optimization is not the priority of this package, the idea is to leave the possibility to any user to implement or even propose implementations related to this kind of problem. The modules are here for the moment empty but present to establish the structure relative to these types of implementations.
+Although continuous optimization is not the priority of this package, the idea is to leave the possibility to any user to implement or even propose implementations related to this kind of problem. The modules are here for the moment nearly empty (only with Zdt functions example) but present to establish the structure relative to these types of implementations.
 
 If a user wishes to propose these developments so that they can be added in a future version of the package, he can refer to the guidelines_ for contributions of the package.
 
@@ -134,7 +134,8 @@ macop.evaluators
 .. autosummary::
    :toctree: macop
    
-   macop.evaluators.continuous
+   macop.evaluators.discrete.mono
+   macop.evaluators.discrete.multi
 
 macop.operators
 -------------------

+ 77 - 0
examples/ZdtExample.py

@@ -0,0 +1,77 @@
+# main imports
+import logging
+import os
+import random
+import numpy as np
+
+# module imports
+from macop.solutions.discrete import BinarySolution
+from macop.evaluators.discrete.mono import UBQPEvaluator
+
+from macop.operators.continuous.mutators import SimpleMutation
+from macop.operators.discrete.mutators import SimpleBinaryMutation
+
+from macop.policies.classicals import RandomPolicy
+
+from macop.algorithms.mono import IteratedLocalSearch as ILS
+from macop.algorithms.mono import HillClimberFirstImprovment
+from macop.callbacks.classicals import BasicCheckpoint
+
+if not os.path.exists('data'):
+    os.makedirs('data')
+
+# logging configuration
+logging.basicConfig(format='%(asctime)s %(message)s', filename='data/example.log', level=logging.DEBUG)
+
+random.seed(42)
+
+# usefull instance data
+n = 100
+ubqp_instance_file = 'instances/ubqp/ubqp_instance.txt'
+filepath = "data/checkpoints_ubqp.csv"
+
+
+# default validator
+def validator(solution):
+    return True
+
+# define init random solution
+def init():
+    return BinarySolution.random(n, validator)
+
+
+filepath = "data/checkpoints.csv"
+
+def main():
+
+    # load UBQP instance
+    with open(ubqp_instance_file, 'r') as f:
+
+        lines = f.readlines()
+
+        # get all string floating point values of matrix
+        Q_data = ''.join([ line.replace('\n', '') for line in lines[8:] ])
+
+        # load the concatenate obtained string
+        Q_matrix = np.fromstring(Q_data, dtype=float, sep=' ').reshape(n, n)
+
+    print(f'Q_matrix shape: {Q_matrix.shape}')
+
+    operators = [SimpleBinaryMutation(), SimpleMutation()]
+    policy = RandomPolicy(operators)
+    callback = BasicCheckpoint(every=5, filepath=filepath)
+    evaluator = UBQPEvaluator(data={'Q': Q_matrix})
+
+    # passing global evaluation param from ILS
+    hcfi = HillClimberFirstImprovment(init, evaluator, operators, policy, validator, maximise=True, verbose=True)
+    algo = ILS(init, evaluator, operators, policy, validator, localSearch=hcfi, maximise=True, verbose=True)
+    
+    # add callback into callback list
+    algo.addCallback(callback)
+
+    bestSol = algo.run(10000, ls_evaluations=100)
+
+    print('Solution for UBQP instance score is {}'.format(evaluator.compute(bestSol)))
+
+if __name__ == "__main__":
+    main()

+ 44 - 0
macop/evaluators/continuous/mono.py

@@ -0,0 +1,44 @@
+"""Mono-objective evaluators classes for continuous problem
+"""
+# main imports
+from macop.evaluators.base import Evaluator
+
+
+class ZdtEvaluator(Evaluator):
+    """Generic Zdt evaluator class which enables to compute custom Zdt function for continuous problem
+
+    - stores into its `_data` dictionary attritute required measures when computing a knapsack solution
+    - `_data['f']` stores lambda Zdt function 
+    - `compute` method enables to compute and associate a score to a given knapsack solution
+
+    Example:
+
+    >>> import random
+    >>>
+    >>> # binary solution import
+    >>> from macop.solutions.continuous import ContinuousSolution
+    >>>
+    >>> # evaluator import
+    >>> from macop.evaluators.continuous.mono import ZdtEvaluator
+    >>> solution_data = [2, 3, 4, 1, 2, 3, 3]
+    >>> size = len(solution_data)
+    >>> solution = ContinuousSolution(solution_data, size)
+    >>>
+    >>> # evaluator initialization (Shere function)
+    >>> f_sphere = lambda s: sum([ x * x for x in s.data])
+    >>> evaluator = ZdtEvaluator(data={'f': f_sphere})
+    >>>
+    >>> # compute solution score
+    >>> evaluator.compute(solution)
+    45
+    """
+    def compute(self, solution):
+        """Apply the computation of fitness from solution
+
+        Args:
+            solution: {:class:`~macop.solutions.base.Solution`} -- Solution instance
+    
+        Returns:
+            {float}: fitness score of solution
+        """
+        return self._data['f'](solution)

+ 4 - 0
macop/evaluators/continuous/multi.py

@@ -0,0 +1,4 @@
+"""Multi-objective evaluators classes for continuous problem
+"""
+# main imports
+from macop.evaluators.base import Evaluator

+ 1 - 1
macop/evaluators/discrete/mono.py

@@ -1,4 +1,4 @@
-"""Mono-objective evaluators classes
+"""Mono-objective evaluators classes for discrete problem
 """
 # main imports
 from macop.evaluators.base import Evaluator

+ 1 - 1
macop/evaluators/discrete/multi.py

@@ -1,4 +1,4 @@
-"""Multi-objective evaluators classes 
+"""Multi-objective evaluators classes for discrete problem
 """
 # main imports
 from macop.evaluators.base import Evaluator

+ 78 - 0
macop/operators/continuous/crossovers.py

@@ -3,6 +3,84 @@
 # main imports
 import random
 import sys
+import numpy as np
 
 # module imports
 from macop.operators.base import Crossover
+
+class BasicDifferentialEvolutionCrossover(Crossover):
+    """Basic Differential Evolution implementation for continuous solution
+
+    Attributes:
+        kind: {:class:`~macop.operators.base.KindOperator`} -- specify the kind of operator
+
+    Example:
+
+    >>> # import of solution and polynomial mutation operator
+    >>> from macop.solutions.continuous import ContinuousSolution
+    >>> from macop.operators.continuous.crossovers import BasicDifferentialEvolutionCrossover
+    >>> solution = ContinuousSolution.random(5, (-2, 2))
+    >>> list(solution.data)
+    [-1.3760219186551894, -1.7676655513272022, 1.4647045830997407, 0.4044600469728352, 0.832290311184182]
+    >>> crossover = BasicDifferentialEvolutionCrossover(interval=(-2, 2))
+    >>> crossover_solution = crossover.apply(solution)
+    >>> list(crossover_solution.data)
+    [-1.7016619497704522, -0.43633033292228895, 2.0, -0.034751768954844, 0.6134819652022994]
+    """
+
+    def __init__(self, interval, CR=1.0, F=0.5):
+        """"Basic Differential Evolution crossover initialiser in order to specify kind of Operator and interval of continuous solution
+
+        Args:
+            interval: {(float, float)} -- minimum and maximum values interval of variables in the solution
+            CR: {float} -- probability to use of new generated solutions when modifying a value of current solution
+            F: {float} -- degree of impact of the new generated solutions on the current solution when obtaining new solution
+        """
+        super().__init__()
+
+        self.mini, self.maxi = interval
+        self.CR = CR
+        self.F = F
+
+
+    def apply(self, solution):
+        """Create new solution based on solution passed as parameter
+
+        Args:
+            solution: {:class:`~macop.solutions.base.Solution`} -- the solution to use for generating new solution
+
+        Returns:
+            {:class:`~macop.solutions.base.Solution`}: new continuous generated solution
+        """
+
+        size = solution.size
+
+        solution1 = solution.clone()
+
+        # create two new random solutions using instance and its static method
+        solution2 = solution.random(size, interval=(self.mini, self.maxi))
+        solution3 = solution.random(size, interval=(self.mini, self.maxi))
+
+        # apply crossover on the new computed solution
+        for i in range(len(solution1.data)):
+
+            # use of CR to change or not the current value of the solution using new solutions
+            if random.uniform(0, 1) < self.CR:
+                solution1.data[i] = solution1.data[i] + self.F * (solution2.data[i] - solution3.data[i])
+
+        # repair solution if necessary
+        solution1.data = self._repair(solution1)
+
+        return solution1
+
+    def _repair(self, solution):
+        """
+        Private repair function for solutions if an element is out of bounds of an expected interval 
+
+        Args:
+            solution: {:class:`~macop.solutions.base.Solution`} -- the solution to use for generating new solution
+
+        Returns:
+            {ndarray} -- repaired array of float values
+        """
+        return np.array([self.mini if x < self.mini else self.maxi if x > self.maxi else x for x in solution.data])

+ 86 - 0
macop/operators/continuous/mutators.py

@@ -3,6 +3,92 @@
 # main imports
 import random
 import sys
+import numpy as np
 
 # module imports
 from macop.operators.base import Mutation
+
+class PolynomialMutation(Mutation):
+    """Polynomial Mutation implementation for continuous solution
+
+    Attributes:
+        kind: {:class:`~macop.operators.base.KindOperator`} -- specify the kind of operator
+
+    Example:
+
+    >>> # import of solution and polynomial mutation operator
+    >>> from macop.solutions.continuous import ContinuousSolution
+    >>> from macop.operators.continuous.mutators import PolynomialMutation
+    >>> solution = ContinuousSolution.random(5, (-2, 2))
+    >>> list(solution.data)
+    [-0.50183952461055, 1.8028572256396647, 0.9279757672456204, 0.3946339367881464, -1.375925438230254]
+    >>> mutator = PolynomialMutation(interval=(-2, 2))
+    >>> mutation_solution = mutator.apply(solution)
+    >>> list(mutation_solution.data)
+    [-0.50183952461055, 1.8028572256396647, 0.9279757672456204, 0.3946339367881464, -1.375925438230254]
+    """
+
+    def __init__(self, interval):
+        """Polynomial Mutation initialiser in order to specify kind of Operator and interval of continuous solution
+
+        Args:
+            interval: {(float, float)} -- minimum and maximum values interval of variables in the solution
+        """
+        super().__init__()
+
+        self.mini, self.maxi = interval
+
+
+    def apply(self, solution):
+        """Create new solution based on solution passed as parameter
+
+        Args:
+            solution: {:class:`~macop.solutions.base.Solution`} -- the solution to use for generating new solution
+
+        Returns:
+            {:class:`~macop.solutions.base.Solution`}: new generated solution
+        """
+
+        size = solution.size
+        rate = float(1/size)
+
+        copy_solution = solution.clone()
+
+        rand = random.uniform(0, 1)
+
+        # apply mutation over the new computed solution
+        copy_solution.data = [x if rand > rate else x + self._sigma(size) * (self.maxi - (self.mini)) for x in solution.data]
+        copy_solution.data = self._repair(copy_solution)
+
+        return copy_solution
+
+    def _repair(self, solution):
+        """
+        Private repair function for solutions if an element is out of bounds of an expected interval
+
+        Args:
+            solution: {:class:`~macop.solutions.base.Solution`} -- the solution to use for generating new solution
+
+        Returns:
+            {ndarray} -- repaired array of float values
+        """
+        return np.array([self.mini if x < self.mini else self.maxi if x > self.maxi else x for x in solution.data])
+
+    
+    def _sigma(self, size):
+        """
+        Compute the sigma value for polynomial mutation
+
+        Args:
+            size: {integer} -- 
+
+        Returns:
+            {float} -- expected sigma value depending on solution size
+        """
+        rand = random.uniform(0, 1)
+        sigma = 0
+        if rand < 0.5:
+            sigma = pow(2 * rand, 1 / (size + 1)) - 1
+        else:
+            sigma = 1 - pow(2 - 2 * rand, 1 / (size - 1))
+        return sigma

+ 3 - 3
macop/operators/discrete/crossovers.py

@@ -53,12 +53,12 @@ class SimpleCrossover(Crossover):
     >>> # using best solution, simple crossover is applied
     >>> best_solution = algo.run(100)
     >>> list(best_solution.data)
-    [1, 1, 0, 1, 1, 1, 1, 1, 0, 0]
+    [1, 1, 0, 1, 1, 1, 0, 1, 1, 1]
     >>> new_solution_1 = initialiser()
     >>> new_solution_2 = initialiser()
     >>> offspring_solution = simple_crossover.apply(new_solution_1, new_solution_2)
     >>> list(offspring_solution.data)
-    [0, 1, 0, 0, 0, 1, 1, 0, 1, 1]
+    [1, 0, 1, 1, 0, 0, 0, 1, 0, 0]
     """
     def apply(self, solution1, solution2=None):
         """Create new solution based on best solution found and solution passed as parameter
@@ -139,7 +139,7 @@ class RandomSplitCrossover(Crossover):
     >>> new_solution_2 = initialiser()
     >>> offspring_solution = random_split_crossover.apply(new_solution_1, new_solution_2)
     >>> list(offspring_solution.data)
-    [1, 0, 0, 1, 1, 1, 0, 0, 1, 1]
+    [0, 0, 0, 1, 1, 1, 1, 0, 0, 1]
     """
     def apply(self, solution1, solution2=None):
         """Create new solution based on best solution found and solution passed as parameter

+ 1 - 1
macop/policies/reinforcement.py

@@ -72,7 +72,7 @@ class UCBPolicy(Policy):
     >>> type(solution).__name__
     'BinarySolution'
     >>> policy.occurences # one more due to first evaluation
-    [53, 50]
+    [50, 52]
     """
     def __init__(self, operators, C=100., exp_rate=0.1):
         """UCB Policy initialiser

+ 85 - 1
macop/solutions/continuous.py

@@ -1,2 +1,86 @@
 """Continuous solution classes implementation
-"""
+"""
+import numpy as np
+
+# modules imports
+from macop.solutions.base import Solution
+
+class ContinuousSolution(Solution):
+    """
+    Continuous solution class
+
+    - store solution as a float array (example: [0.5, 0.2, 0.17, 0.68, 0.42])
+    - associated size is the size of the array
+    - mainly use for selecting or not an element in a list of valuable objects
+
+    Attributes:
+        data: {ndarray} --  array of float values
+        size: {int} -- size of float array values
+        score: {float} -- fitness score value
+    """
+    def __init__(self, data, size):
+        """
+        initialise continuous solution using specific data
+
+        Args:
+            data: {ndarray} --  array of float values
+            size: {int} -- size of float array values
+
+        Example:
+
+        >>> from macop.solutions.continuous import ContinuousSolution
+        >>>
+        >>> # build of a solution using specific data and size
+        >>> data = [0.2, 0.4, 0.6, 0.8, 1]
+        >>> solution = ContinuousSolution(data, len(data))
+        >>>
+        >>> # check data content
+        >>> sum(solution.data) == 3
+        True
+        >>> # clone solution
+        >>> solution_copy = solution.clone()
+        >>> all(solution_copy.data == solution.data)
+        True
+        """
+        super().__init__(np.array(data), size)
+
+    @staticmethod
+    def random(size, interval, validator=None):
+        """
+        Intialize float array with use of validator to generate valid random solution
+
+        Args:
+            size: {int} -- expected solution size to generate
+            interval: {(float, float)} -- tuple with min and max expected interval value for current solution
+            validator: {function} -- specific function which validates or not a solution (if None, not validation is applied)
+
+        Returns:
+            {:class:`~macop.solutions.discrete.Continuous`}: new generated continuous solution
+
+        Example:
+
+        >>> from macop.solutions.continuous import ContinuousSolution
+        >>>
+        >>> # generate random solution using specific validator
+        >>> validator = lambda solution: True if sum(solution.data) > 5 else False
+        >>> solution = ContinuousSolution.random(10, (-2, 2), validator)
+        >>> sum(solution.data) > 5
+        True
+        """
+
+        mini, maxi = interval
+
+        data = np.random.random(size=size) * (maxi - mini) + mini
+        solution = ContinuousSolution(data, size)
+
+        if not validator:
+            return solution
+
+        while not validator(solution):
+            data = np.random.random(size=size) * (maxi - mini) + mini
+            solution = ContinuousSolution(data, size)
+
+        return solution
+
+    def __str__(self):
+        return f"Continuous solution {self._data}"

+ 9 - 0
setup.py

@@ -21,6 +21,9 @@ class TestCommand(distutils.command.check.check):
         from macop.operators.discrete import mutators as discrete_mutators
         from macop.operators.discrete import crossovers as discrete_crossovers
 
+        from macop.operators.continuous import mutators as continuous_mutators
+        from macop.operators.continuous import crossovers as continuous_crossovers
+
         # policies module
         from macop.policies import classicals
         from macop.policies import reinforcement
@@ -49,6 +52,12 @@ class TestCommand(distutils.command.check.check):
         doctest.testmod(discrete_mutators)
         doctest.testmod(discrete_crossovers)
 
+        random.seed(42)
+        np.random.seed(42)
+        # operators module
+        doctest.testmod(continuous_mutators)
+        doctest.testmod(continuous_crossovers)
+
         random.seed(42)
         np.random.seed(42)
         # policies module