|
GeneticAlgorithm
0.5 (beta)
A python framework for rapid GA prototyping
|
00001 import copy 00002 import random 00003 00004 00005 ## @class GABaseObject 00006 # @brief The base object provides the functions repr and str 00007 # 00008 # The GABaseObject provides a custom behavior for GA objects, which allows for a useful repr() and str() implementation. 00009 # Because every object on this library is derived from this class, the whole scope of the library can be appreciated in this class' inheritance diagram. 00010 class GABaseObject(object): 00011 ## @__init__(self, **kwargs) 00012 # @brief The default behavior for GA objects 00013 # 00014 # The default behavior of GA objects is to accept any kind of named arguments on their constructor, and save them in properties with the same name. 00015 def __init__(self, **kwargs): 00016 for param, val in kwargs.items(): 00017 self.__setattr__(param, val) 00018 ## @fn __repr__(self) 00019 # @brief The string representation of the object 00020 # 00021 # The default behavior of GA objects is return representations as the Python code needed to reproduce the instance self 00022 def __repr__(self): 00023 return '%(class)s(%(params)s)' % \ 00024 { 'class':type(self).__name__, \ 00025 'params':', '.join( [str(prop)+' = '+ repr(value) for prop, value in vars(self).iteritems()] ) } 00026 ## @fn __str__(self) 00027 # @brief The string representation of the object 00028 # 00029 # The default str() representation of GA objects is to return repr() 00030 def __str__(self): 00031 return repr(self) 00032 00033 ## @class GeneticOperator 00034 # @brief GeneticOperator is the base class that should be used to implement selection, crossover and mutation 00035 # 00036 # The class GeneticAlgorithm is a scheduler for that periodically uses objects of this kind to transform the current population. This class can be derived to perform any activity, inclyuding but not limited to: 00037 # <ul> 00038 # <li>Logging, computing statistics about populations</li> 00039 # <li>Interpretation or decoding: Genotype \f$ \rightarrow \f$ Phenotype</li> 00040 # <li>Evaluation: Phenotype \f$ \rightarrow \f$ fitness</li> 00041 # <li>Crossover: Genotype \f$\times\f$ Genotype \f$ \rightarrow \f$ Genotype</li> 00042 # <li>Mutation: Genotype \f$ \rightarrow \f$ Genotype</li> 00043 # </ul> 00044 # Every genetic operator may overload any combination of these functions: 00045 # <ul> 00046 # <li>initialize</li> 00047 # <li>iterate</li> 00048 # <li>finalize</li> 00049 # </ul> 00050 # All of these functions receive a population object, which is expected to be modified by the function in order to implement the desired transformation. 00051 # Please note that a single operator could implement a full genetic algorithm, but modularization is encouraged to allow easy re-use of operators 00052 class GeneticOperator(GABaseObject): 00053 ## @fn initialize(population) 00054 # @param population The population object to operate upon 00055 # @brief This function is called once, during the startup phase of the algorithm 00056 def initialize(self, population): 00057 pass 00058 ## @fn iterate(population) 00059 # @param population The population object to operate upon 00060 # @brief This function is called once every iteration 00061 def iterate(self, population): 00062 pass 00063 ## @fn finalize(population) 00064 # @param population The population object to operate upon 00065 # @brief This function is called once at the end of the algorithm run 00066 def finalize(self, population): 00067 pass 00068 00069 ## @class BasePeriodicOperator 00070 # @brief A class that serves as a base for all periodic operators 00071 # 00072 # This class automatically counts the number iterate has been called, and the number of individuals that have been evaluated so far. 00073 # The operator calls the iterationCallback function whenever the iteration call count is an integer multiple of iterationFrequency. 00074 # The operator calls the evaluationCallback function whenever the number of evaluated individuals is an integer multiple of evaluationFrequency 00075 class BasePeriodicOperator(GeneticOperator): 00076 ## @fn __init__(self, iterationCounter=0, evaluationCounter=0, iterationFrequency=0, evaluationFrequency=0, **kwargs) 00077 # @brief Base periodic logger constructor 00078 # @param iterationCounter An initial value for the iteration counter 00079 # @param evaluationCounter An initial value for the individual evaluation counter 00080 # @param iterationFrequency This parameter controls how many iterations have to pass between iterationCallback calls 00081 # @param evaluationFrequency This parameter controls how many individuals are evaluated between evaluationCallback calls 00082 def __init__(self, **kwargs): 00083 super(BasePeriodicOperator, self).__init__(**kwargs) 00084 self.iterationCounter = kwargs.get('iterationCounter' , 0) 00085 self.evaluationCounter = kwargs.get('evaluationCounter' , 0) 00086 self.iterationFrequency = kwargs.get('iterationFrequency' , None) 00087 self.evaluationFrequency = kwargs.get('evaluationFrequency', None) 00088 00089 00090 ## @fn iterationCallback(self, population) 00091 # @brief Overload this function to perform logging every time that the iterationCounter counter is an integer multiple of iterationFrequency 00092 def iterationCallback(self, population): 00093 pass 00094 00095 ## @fn evaluationCallback(self, population) 00096 # @brief Overload this function to perform logging every time that the evaluationCounter is an integer multiple of evaluationFrequency 00097 def evaluationCallback(self, population): 00098 pass 00099 00100 ## @fn iterate(self, population) 00101 # This function counts the number of times it has been called, and the number of individuals that have been evaluated so far, and calls the iterationCallback and evaluatioLogger functions accordingly 00102 def iterate(self, population): 00103 # Update the iteration counter and call the iterationCallback function whenever (self.iterationCounter % self.iterationFrequency) == 0 00104 self.iterationCounter += 1 00105 # Update the evaluationCounter and call the evaluationCallback whenever (self.evaluationCounter % self.evaluationFrequency)==0 00106 self.evaluationCounter += getattr(population, 'genSize', len(population.individuals) ) 00107 if self.iterationFrequency and ((self.iterationCounter % self.iterationFrequency) == 0): 00108 self.iterationCallback(population) 00109 if self.evaluationFrequency and ((self.evaluationCounter % self.evaluationFrequency)==0): 00110 self.evaluationCallback(population) 00111 00112 00113 ## @class Mutate 00114 # @brief Mutate an individual 00115 class Mutate(GeneticOperator): 00116 def mutate(self, population): 00117 pm = getattr(population, 'mutation_probability', 0.01 ) 00118 # Evaluate only recently generated items (pointed to by population.lethals) 00119 lethals = getattr(population, 'lethals', None ) 00120 # If population.lethals does not exist, update every individual (and set the lethals list to contain every index) 00121 if not lethals: 00122 lethals = range(len(population.individuals)) 00123 # Iterate over recently replaced individuals 00124 for i in lethals: 00125 if random.random() < pm: 00126 population.individuals[i].mutate() 00127 iterate = mutate 00128 00129 ## @class Crossover 00130 # @brief Crossover two individuals from population.matingPool with probability population.crossoverProbability 00131 class Crossover(GeneticOperator): 00132 def cross(self, population): 00133 pc = getattr(population, 'crossover_probability', 1.0 ) 00134 # Get the vector of pointers to lethals from population 00135 lethals = getattr(population, 'lethals', None) 00136 # If no vector is available, then all individuals are scheduled for replacement 00137 if not lethals: 00138 lethals = xrange(len(population.individuals)) 00139 # Get the number of individuals to replace 00140 nLethals = len(lethals) 00141 # Initialize a list of offspring vectors 00142 offspring = [None] * nLethals 00143 # Get the mating pool from the population 00144 matingPool = getattr(population, 'matingPool', None) 00145 # If no mating pool is available, raise an error 00146 if not matingPool: 00147 raise RuntimeError('No mating pool found on population, a selection operator must come before Crossover') 00148 # Generate the offspring and insert them in different loops, to conserve the parents unchanged for crossover 00149 for i in xrange(nLethals): 00150 offspring[i] = population.individuals[ matingPool[0] ] 00151 if random.random() < pc: 00152 offspring[i] = offspring[i].crossover( population.individuals[matingPool[1]] ) 00153 # Make a deep copy of the new individual, to avoid a single segment to be referenced by several genotypes 00154 offspring[i] = copy.deepcopy(offspring[i]) 00155 # Remove the first two parents from the mating pool 00156 matingPool = matingPool[2:] 00157 # Insert the offspring in the population 00158 for i, o in zip(lethals, offspring): 00159 population.individuals[i] = o 00160 iterate = cross; 00161 00162 ## @class BaseChromosomeSegment 00163 # @brief This class defines the minimal expression of a chromosome segment. 00164 # 00165 # This class defines the minimal expresion of a chromosome. This interface contains: 00166 # <ul> 00167 # <li>data: The data contained in this segment</li> 00168 # <li>crossover: Implements the algorithm to combine two chromosome segments</li> 00169 # <li>mutation: Implements the algorithm to mutate this segment</li> 00170 # <li>randomize: Assign a valid, random value to self.data</li> 00171 # </ul> 00172 class BaseChromosomeSegment(GABaseObject): 00173 ## @fn __init__(self, data=0, **kwargs) 00174 # @brief A constructor that takes data and any number of named arguments, and adds all properties to the object 00175 # @param data The data to assign to this segment 00176 # 00177 # Deriving from this class guarantees the following basic behavior, which is assumed by other GAObject classes: 00178 # 00179 # <ul> 00180 # <li> The data property contains will be used by decode functions to produce a Penotype </li> 00181 # <li> The constructor generates a random value for data whenever its not provided </li> 00182 # <li> The constructor accepts any number of named arguments, which will be stored in properties of the same name</li> 00183 # </ul> 00184 # 00185 # The methods randomize, crossover and mutate must be re implemented on every specialization of this class. 00186 def __init__(self, data=None, **kwargs): 00187 # Set all named properties first 00188 super(BaseChromosomeSegment, self).__init__(**kwargs) 00189 ## @property data This property returns the data used to produce a Genotype 00190 if data==None: 00191 # If no data was provided, randomize the chromosome segment 00192 self.randomize() 00193 else: 00194 ## @property data 00195 # @brief The property that stores the chromosome segment value 00196 self.data = data 00197 00198 ## @fn randomize(self) 00199 # @brief Assign a valid, random value to self.data 00200 def randomize(self): 00201 pass 00202 00203 ## @fn crossover(self, other) 00204 # @param other Another chromosome segment to be combined with self 00205 # @brief This function is the crossover operator interface, and must be implemented for the default crossover functions to work 00206 # @note It is recommended that all specializations of this function return a new object, sing the classes Genotype and Individual are containers and handle refereces exclusively. The generation of new chromosome segments is always delegated to this and the constructor functions. 00207 def crossover(self, other): 00208 pass 00209 ## @fn mutate(self) 00210 # @brief This function is the mutation operator interface, and must be implemented for the default mutation functions to work 00211 def mutate(self, chromo): 00212 pass 00213 00214 ## @class Genotype 00215 # @brief The base Genotype class from which all chromosomes must derived 00216 # 00217 # The Genotype class is primarily a container that supports the minimum GA interface: 00218 # <ul> 00219 # <li>randomize(self)</li> 00220 # <li>mutate(self)</li> 00221 # <li>crossover(self, other)</li> 00222 # </ul> 00223 class Genotype(GABaseObject): 00224 ## @fn init(segments=[]) 00225 # @brief Initialize the genotype 00226 # @param segments A list of BaseChromosomeSegment or derived objects 00227 def __init__(self, segments=[]): 00228 self.segments = segments 00229 ## @fn __setattr__(self, attribute, value) 00230 # @brief This function allows typechecking to occur for segments, without the need for accessors 00231 def __setattr__(self, attribute, value): 00232 if attribute == 'segments': 00233 super(Genotype, self).__setattr__(attribute, []) 00234 for seg in value: 00235 self.addSegment(seg) 00236 else: 00237 super(Genotype, self).__setattr__(attribute, value) 00238 ## @fn randomie(self) 00239 # @brief Assign a random value to every segment 00240 def randomize(self): 00241 for s in self.segments: 00242 s.randomize() 00243 ## @fn addSegment(self, segment) 00244 # @brief Add a segment to the genotype. 00245 def addSegment(self, segment): 00246 if isinstance(segment, BaseChromosomeSegment): 00247 self.segments.append(segment) 00248 ## @fn crossover(self, other) 00249 # @brief Perform a one-point crossover between self an and other Genotype 00250 # @return A Genotype object that contains the new genotype 00251 # @warning Segments are references to objects. It is recommended that 00252 def crossover(self, other): 00253 crossPoint = random.randrange( len(self.segments) ) 00254 return Genotype( self.segments[:crossPoint] + \ 00255 [ self.segments[crossPoint].crossover(other.segments[crossPoint]) ] + \ 00256 other.segments[crossPoint+1:] ) 00257 ## @fn mutate(self) 00258 # @brief Select one segment randomly and call mutate() on it 00259 def mutate(self): 00260 mutant = random.randrange( len(self.segments) ) 00261 self.segments[mutant].mutate() 00262 ## @fn __str__(self) 00263 # 00264 def __str__(self): 00265 return '[%s]' % ', '.join([str(s) for s in self.segments]) 00266 00267 ## @example GABaseObject-demo.py 00268 # This example shows the usage model for the GABaseObject class 00269 ## @class Individual 00270 # @brief This class is a container intended to store all information relevant to an individual. 00271 # 00272 # This class is a container, intended to store all information relevant to an individual, including but not limited to: 00273 # <ul> 00274 # <li>Genotype</li> 00275 # <li>Phenotype</li> 00276 # <li>Evaluation</li> 00277 # <li>Non-generational information, such as:</li> 00278 # <ul> 00279 # <li>Age</li> 00280 # <li>Fitness moving average</li> 00281 # <li>Fitness variance</li> 00282 # </ul> 00283 # </ul> 00284 class Individual(GABaseObject): 00285 ## @fn __init__(self, genotype=[], **kwargs) 00286 # @brief The individual constructor 00287 # 00288 # The minimum expression of an individual is to have a genotype and fitness value. Any other imaginable property can be added at this stage by passing a named parameter 00289 def __init__(self, genotype=Genotype(), fitness=0.0, **kwargs): 00290 super(Individual, self).__init__(genotype=genotype, fitness=fitness, **kwargs) 00291 00292 ## @fn randomize(self) 00293 # @brief Call self.genotype.randomize() 00294 def randomize(self): 00295 self.genotype.randomize() 00296 00297 ## @fn valuesToStr(self) 00298 # @brief Return the string representation of the values of all properties 00299 # @param separator The string that separates property values 00300 # @return The string representation of the values of all properties separated by the optional separator string 00301 def valuesToStr(self, separator='\t'): 00302 return separator.join( [str(value) for value in vars(self).itervalues()] ) 00303 00304 ## @fn propertiesToStr(self) 00305 # @brief Return the names of all properties in the order that valuesToStr prints them 00306 # @param separator The string that separates property names 00307 # @return The names properties separated by the optional separator string 00308 def propertiesToStr(self, separator='\t'): 00309 return separator.join( [str(prop) for prop in vars(self).iterkeys()] ) 00310 00311 ## @fn __str__(self) 00312 # @brief Return a string representation of self, accordin to the function toStr 00313 # 00314 # The behavior of this function can be easily changed with the following technique: 00315 # @code 00316 # Individual.__str__ = lambda(self) : self.toStr(propertyList=['property1', 'property2', ...]) 00317 # @endcode 00318 # Where the propertyList is a list of strings containing the names of the properties to display 00319 def __str__(self): 00320 return self.valuesToStr() 00321 00322 ## @fn crossover(self, other) 00323 # @brief Crossover self and another genotype 00324 # @return an Individual object containing the crossover of self and other 00325 def crossover(self, other): 00326 offspring = copy.deepcopy(self) 00327 offspring.genotype = offspring.genotype.crossover( other.genotype ) 00328 return offspring 00329 00330 ## @fn mutate 00331 # @brief call mutate() on self's chromosome 00332 def mutate(self): 00333 self.genotype.mutate() 00334 00335 ## @class Population 00336 # @brief This class is a container, intended to store references to individuals and all their 00337 class Population(GABaseObject): 00338 ## @fn __init__(self, individuals=[], individualSchema=Genotype(), **kwargs) 00339 # @brief The Population constructor 00340 # @param individuals A list of Individual objects 00341 # @param schema A Genotype that will be used as template to produce the genotypes of the population 00342 # @parap popSize If popSize is provided, self.populate(popSize) and self.randomize() are called after initialization, note that this overrides the value passed to individuals 00343 def __init__(self, name='', individuals=[], schema=Genotype(), popSize=None, maximize=True, **kwargs): 00344 super(Population, self).__init__(name=name, individuals=individuals, schema=schema, maximize=maximize, **kwargs) 00345 if popSize: 00346 self.populate(popSize) 00347 self.randomize() 00348 00349 ## @fn __str__(self) 00350 # @brief The string representation of the population 00351 # 00352 # This function prints all variables associated with self. Note that any user-defined property will be printed as well 00353 def __str__(self): 00354 msg = [ self.name ] 00355 for prop, value in vars(self).iteritems(): 00356 if prop == 'name' or prop == 'schema' or prop == 'individuals' : 00357 continue 00358 else: 00359 msg+=[ str(prop) + ': ' + str(value) ] 00360 msg+= [ 'schema: ' + repr(self.schema) ] 00361 msg+= [ 'Individuals: (' + self.individuals[0].propertiesToStr(',') + ')' ] + [str(i) + '\t' + str(ind) for i, ind in enumerate(self.individuals) ] 00362 return '\n'.join(msg) 00363 00364 ## @fn populate(n=100) 00365 # @brief Generate the list of individuals by copying the schema n times 00366 # @param n The number of individuals to contain in the population 00367 def populate(self, n=100): 00368 self.individuals = [ Individual(genotype=copy.deepcopy(self.schema)) for i in xrange(n) ] 00369 00370 ## @fn randomize(self) 00371 # @brief Randomize the population by calling randomize on each individual 00372 def randomize(self): 00373 for individual in self.individuals: 00374 individual.randomize() 00375 00376 ## @class Scheduler 00377 # @brief A class that encapsulates a Population and a list of GeneticOperator 00378 # 00379 # The class GAShceduler encapsulates a list of GeneticOperator objects and a population. 00380 # This class is designed to be the highest level of abstraction of a GA. The scheduler applies the genetic operators in order, and provides the following interface: 00381 # 00382 # <ul> 00383 # <li>initialize</li> 00384 # <li>iterate</li> 00385 # <li>initialize</li> 00386 # <ul> 00387 class Scheduler(GABaseObject): 00388 ## @fn __init__(self, name='Untitled', operators=[], population=Population()): 00389 # @brief GAScheduler 00390 def __init__(self, name='Untitled', operators=[], population=Population(), **kwargs): 00391 super(Scheduler, self).__init__(name=name, operators=operators, population=population, **kwargs) 00392 ## @fn __str__(self) 00393 # @brief Return the string representation of the scheduler 00394 def __str__(self): 00395 msg = ['name: ' + str(self.name)] 00396 msg+= ['\n'.join(['operators:'] + [str(o) for o in self.operators])] 00397 msg+= ['population: ' + str(self.population)] 00398 return '\n'.join(msg) 00399 00400 ## @fn initialize(self) 00401 # @brief This function is called at the begining of the runGA method 00402 # 00403 # This function calls the initialize function of every operator on population once. Genetic operators are expected to initialize any variables 00404 def initialize(self): 00405 for o in self.operators: 00406 o.initialize(self.population) 00407 00408 ## @fn iterate(self) 00409 # @brief This function is called every iteration of the runGA method 00410 # 00411 # Every operator iterate method over self.population 00412 def iterate(self): 00413 for o in self.operators: 00414 o.iterate(self.population) 00415 00416 ## @fn finalize(self) 00417 # @brief This function is called once at the end of the runGA method 00418 # 00419 # Callthe finalize method of every operator at the end of runGA 00420 def finalize(self): 00421 for o in self.operators: 00422 o.finalize(self.population) 00423 00424 ## @fn runGA 00425 # @brief Initialize, run n iterations and finalize the GA run 00426 # @param n The number of iterations to run 00427 def runGA(self, n): 00428 self.initialize() 00429 for i in xrange(n): 00430 self.iterate() 00431 self.finalize()