├── MANIFEST.in ├── .gitignore ├── fastga ├── __init__.py ├── utils.py ├── sampling.py └── operators.py ├── LICENSE.txt ├── setup.py └── README.rst /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | include *.txt 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | build/ 3 | dist/ 4 | fastga.egg-info/ 5 | -------------------------------------------------------------------------------- /fastga/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | This module contains an implementation of our "heavy-tailed mutation operator, 5 | along with a few utility functions and the classical (1+1) EA. 6 | """ 7 | 8 | __version__ = '1.0' 9 | 10 | from fastga.operators import * 11 | 12 | -------------------------------------------------------------------------------- /fastga/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | A few utilitary functions. 3 | """ 4 | 5 | 6 | def flip(array, i): 7 | """ 8 | 'array' should be an indexable object holding only 0s and 1s. 9 | This function flips the i-th value in 'array' : if it is 0, it becomes 1, and vice-versa. 10 | """ 11 | array[i] ^= 1 12 | 13 | 14 | def wrong_param_error(value, supported_values=None): 15 | """ 16 | Returns a nicely-formatted error when a given parameter is not of valid value for some function. 17 | """ 18 | string = "Wrong parameter value '{}'".format(value) 19 | if supported_values is not None: 20 | string += (" ; expected values are {{{}}}" 21 | .format(", ".join(["{}".format(supported) for supported in supported_values]))) 22 | return ValueError(string + ".") 23 | 24 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | 26 | -------------------------------------------------------------------------------- /fastga/sampling.py: -------------------------------------------------------------------------------- 1 | from numpy import log1p 2 | from numpy.random import rand 3 | 4 | 5 | __all__ = ["sample", "sample_waiting_time"] 6 | 7 | 8 | def sample(cdf): 9 | """ 10 | Samples a random positive bounded integer according to the Cumulative Distribution Function 11 | given as input (under the form of an array 'cdf', with cdf[i + 1] >= cdf[i] 12 | and cdf[-1] = 1. Note that these verifications are not performed by the function). 13 | 14 | The resulting integer X has the property that 15 | Pr(X <= i + 1) = cdf[i], for i in [0, ..., n - 1] : 16 | 17 | Pr(X = 1) = cdf[0] 18 | Pr(X = 2) = cdf[1] - cdf[0] 19 | ... 20 | Pr(X = n) = cdf[n - 1] - cdf[n - 2] - ... - cdf[0] 21 | 22 | """ 23 | n = len(cdf) 24 | x = rand() 25 | if x < cdf[0]: 26 | return 1 27 | low = 0 28 | high = n - 1 29 | while high - low > 1: 30 | mid = (low + high) >> 1 # same as (low + high) // 2, but hopefully (mildly) faster 31 | if x > cdf[mid]: 32 | low = mid 33 | else: 34 | high = mid 35 | return high + 1 36 | 37 | 38 | def sample_waiting_time(p): 39 | """ 40 | Samples the number of independent tries until a given random event happens. 41 | 'p' should be the probability of the event happening in one try. 42 | """ 43 | x = rand() 44 | # We use log1p(z) to compute precisely log(1 + z) : 45 | return 1 + int(log1p(- x) // log1p(- p)) 46 | 47 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup, find_packages 5 | 6 | __version__ = '1.0.1' 7 | 8 | min_numpy_ver = '1.7.0' 9 | 10 | setup( 11 | 12 | name='fastga', 13 | version=__version__, 14 | 15 | author='The FastGA team', 16 | 17 | license="Unlicense", 18 | 19 | packages=find_packages(), 20 | 21 | description="Implementation of the so-called Heavy-Tailed Mutation Operator", 22 | 23 | long_description=open('README.rst').read(), 24 | 25 | install_requires=["numpy >= {}".format(min_numpy_ver)], 26 | 27 | # Active la prise en compte du fichier MANIFEST.in 28 | include_package_data=True, 29 | 30 | # Une url qui pointe vers la page officielle de votre lib 31 | url='https://github.com/FastGA/fast-genetic-algorithms', 32 | 33 | # Il est d'usage de mettre quelques metadata à propos de sa lib 34 | # Pour que les robots puissent facilement la classer. 35 | # La liste des marqueurs autorisées est longue: 36 | # https://pypi.python.org/pypi?%3Aaction=list_classifiers. 37 | # 38 | # Il n'y a pas vraiment de règle pour le contenu. Chacun fait un peu 39 | # comme il le sent. Il y en a qui ne mettent rien. 40 | classifiers=[ 41 | "Programming Language :: Python", 42 | "Operating System :: OS Independent", 43 | "License :: Public Domain", 44 | "Programming Language :: Python :: 3.5", 45 | "Topic :: Scientific/Engineering :: Artificial Intelligence" 46 | ], 47 | ) 48 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | This repository holds the code base supporting the article "Fast genetic algorithms" by Benjamin Doerr, Huu Phuoc Le, Régis Makhmara, and Ta Duy Nguyen. The conference version of this work is accepted for publication in the proceedings of GECCO 2017. An extended version can be found on the ArXiV at https://arxiv.org/abs/1703.03334. 2 | 3 | **fastga** is an extremely small module sporting our so-called *fast mutation operator* (or *heavy-tailed mutation operator*), which is designed to perform bitwise mutation using a power-law-distributed mutation rate. This allows a high number of bits to be flipped in one step with high probability (compared to the classical (1+1) EA for example), which is especially desirable when such long-distance "jumps" are necessary to escape local optima. 4 | 5 | Requirements 6 | ============ 7 | 8 | - python3+ 9 | - NumPy 10 | 11 | Installation 12 | ============ 13 | 14 | The pip way (recommended) 15 | ------------------------- 16 | 17 | **(Optionnal)** Create a python virtual environment and activate it. In a console, type : 18 | 19 | :: 20 | 21 | $ virtualenv ~/some_convenient_location/fastga 22 | $ cd ~/some_convenient_location/fastga 23 | $ source bin/activate 24 | 25 | In a console (with your virtualenv activated if you use one), type : 26 | 27 | :: 28 | 29 | $ pip install fastga 30 | 31 | The git way 32 | ----------- 33 | 34 | Simply clone this repository to a convenient location : 35 | 36 | :: 37 | 38 | $ mkdir some_convenient_location && cd some_convenient_location 39 | $ git clone https://github.com/FastGA/fast-genetic-algorithms.git 40 | 41 | then add it to your **PYTHONPATH** : 42 | 43 | :: 44 | 45 | $ export PYTHONPATH=some_convenient_location:$PYTHONPATH 46 | 47 | (you can also put this command in your .bashrc file to make it permanent). 48 | 49 | Usage 50 | ===== 51 | 52 | Our mutation operator is implemented in the class ``FastMutationOperator``, along with the abstract class ``BaseMutationOperator`` (which you shouldn't use directly but rather subclass to your own classes if needed) and the class ``OnePlusOneMutationOperator`` (which, as the name suggests, is an implementation of the (1+1) EA). In a python shell, type 53 | 54 | :: 55 | 56 | from fastga import FastMutationOperator 57 | 58 | Two parameters are required to create an instance : 59 | 60 | - an integer ``n`` which is the size of the bit strings that can be mutated by the operator ; 61 | - a float ``beta`` > 1 which is the exponent used in the mutation rate power law. 62 | 63 | Given these two parameters, the operator's mutation rate *r* is such that, for each *i* in {1 .. n//2}, the probability that *r* is *i/n* is proportional to i^{-beta} (with a suitable normalization factor). As such, lower values of beta tend to favor a higher number of bits flipped in one mutation step. 64 | 65 | You can now instantiate an operator : 66 | 67 | :: 68 | 69 | operator = FastMutationOperator(n=100, beta=1.5) 70 | 71 | and use its ``mutate`` method to mutate *n*-length bit strings : 72 | 73 | :: 74 | 75 | bit_string = [0] * 100 76 | for i in range(10): 77 | operator.mutate(bit_string, inplace=True) 78 | print(bit_string) 79 | -------------------------------------------------------------------------------- /fastga/operators.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implementation of the different mutation operators. 3 | 4 | BaseMutationOperator is an abstract class that sports the basic methods to perform the bitwise mutation of a bit string, 5 | OnePlusOneMutationOperator is an implementation of the classical (1+1) EA, 6 | FastMutationOperator is an implementation of our operator. 7 | 8 | The inheritance scheme is 9 | 10 | BaseMutationOperator 11 | |- OnePlusOneMutationOperator 12 | |- FastMutationOperator 13 | 14 | and a sample usage is : 15 | 16 | oneplusone = OnePlusOneMutationOperator(n=100) 17 | fast = FastMutationOperator(n=100, beta=1.5) 18 | 19 | """ 20 | 21 | from numpy import linspace 22 | from fastga.utils import flip, wrong_param_error 23 | from fastga.sampling import sample, sample_waiting_time 24 | 25 | 26 | __all__ = ["BaseMutationOperator", "OnePlusOneMutationOperator", "FastMutationOperator"] 27 | 28 | 29 | class BaseMutationOperator: 30 | """ 31 | This abstract class holds the common methods that one would expect in a mutation operator. 32 | It should not be used directly. 33 | """ 34 | 35 | def __init__(self, n): 36 | if not isinstance(n, int) or n < 1: 37 | raise ValueError("Parameter 'n' must be a positive integer. Received {}.".format(n)) 38 | self.n = n 39 | 40 | def get_mutation_rate(self): 41 | raise NotImplementedError() # this should be implemented in subclasses 42 | 43 | def _mutate(self, bit_string): 44 | """ 45 | Private 'mutate' method, not to be called externally. Performs an inplace mutation of 46 | the input bit string. A naive implementation would be to go through the n bits of the 47 | bit string and flip them with probability given by the mutation rate. 48 | Alternatively, one can sample the indexes of bits to flip, and flip exactly those. This 49 | is what we do here. 50 | """ 51 | n = self.n 52 | mutation_rate = self.get_mutation_rate() 53 | index = sample_waiting_time(mutation_rate) - 1 # We need to subtract 1 to get a chance 54 | # to mutate the first index. 55 | while index < n: 56 | flip(bit_string, index) 57 | index += sample_waiting_time(mutation_rate) 58 | 59 | def mutate(self, bit_string, inplace=False): 60 | """ 61 | Public 'mutate' method ; can perform the mutation inplace or return a (new) 62 | mutated bit string. 63 | """ 64 | n = self.n 65 | bit_string_len = len(bit_string) 66 | if not bit_string_len == n: 67 | raise ValueError("Wrong length for input bit string : expected {}, got {}." 68 | .format(n, bit_string_len)) 69 | if not inplace: 70 | bit_string = bit_string.copy() 71 | self._mutate(bit_string) 72 | if not inplace: 73 | return bit_string 74 | 75 | 76 | class OnePlusOneMutationOperator(BaseMutationOperator): 77 | 78 | def __init__(self, n): 79 | super().__init__(n) 80 | 81 | def get_mutation_rate(self): 82 | return 1 / self.n 83 | 84 | 85 | class FastMutationOperator(BaseMutationOperator): 86 | 87 | def __init__(self, n, beta): 88 | super().__init__(n) 89 | if beta <= 1: 90 | raise ValueError("Wrong value for parameter beta : received {}, must be a float > 1." 91 | .format(beta)) 92 | self.beta = beta 93 | # We pre-compute the power law cumulative distribution function with the given parameter 94 | # beta. It will allow us to sample random integers according to this distribution. 95 | power_law = (linspace(1, n//2, n//2) ** (- beta)).cumsum() 96 | power_law /= power_law[-1] 97 | self.power_law = power_law 98 | 99 | def get_mutation_rate(self): 100 | r = sample(self.power_law) 101 | return r / self.n 102 | 103 | 104 | if __name__ == '__main__': 105 | fast = FastMutationOperator(10, 1.5) 106 | bit_string = [0] * 10 107 | print(bit_string) 108 | for i in range(10): 109 | fast.mutate(bit_string, inplace=True) 110 | print(bit_string) 111 | 112 | --------------------------------------------------------------------------------