/[pyrobot]/trunk/brain/ga.py
ViewVC logotype

Contents of /trunk/brain/ga.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2488 - (show annotations) (download) (as text)
Sat Apr 26 02:19:13 2008 UTC (21 months, 2 weeks ago) by dblank
File MIME type: text/x-python
File size: 26462 byte(s)
Keeps track of best position, too
1 """
2 A simple Genetic Algorithm in Python
3 """
4
5 __author__ = "Douglas Blank <dblank@brynmawr.edu>"
6 __version__ = "$Revision$"
7
8 import Numeric, math, random, time, sys, string
9 from copy import deepcopy
10
11 def display(v):
12 print v,
13
14 def sum(a):
15 sum = 0
16 for n in a:
17 sum += n
18 return sum
19
20 def flip(probability):
21 """
22 Flip a biased coin
23 """
24 return random.random() <= probability
25
26
27 # The classes:
28 # Gene - specifics of gene representation
29 # Population - collection of Genes
30 # GA - the parameters of evolution
31
32 class Gene:
33 def __init__(self, **args):
34 self.verbose = 0
35 self.genotype = []
36 self.fitness = 0.0
37 self.mode = 'float'
38 self.crossoverPoints = 1
39 self.bias = 0.5
40 self.min = -1 # inclusive
41 self.max = 1 # inclusive
42 self.imin = -1 # inclusive, initial
43 self.imax = 1 # inclusive, initial
44 self.maxStep = 1
45 self.args = args
46 self.alphabet = "abcdefghijklmnopqrstuvwxyz "
47 if args.has_key('verbose'):
48 self.verbose = args['verbose']
49 if args.has_key('min'):
50 self.min = args['min']
51 self.imin = args['min']
52 if args.has_key('max'):
53 self.max = args['max']
54 self.imax = args['max']
55 if args.has_key('imin'):
56 self.imin = args['imin']
57 if args.has_key('imax'):
58 self.imax = args['imax']
59 if args.has_key('maxStep'):
60 self.maxStep = args['maxStep']
61 if args.has_key('crossoverPoints'):
62 self.crossoverPoints = args['crossoverPoints']
63 if args.has_key('mode'):
64 self.mode = args['mode']
65 if args.has_key('bias'):
66 self.bias = args['bias']
67 for i in range(args['size']):
68 if self.mode == 'bit':
69 self.genotype.append( random.random() < self.bias)
70 elif self.mode == 'integer':
71 self.genotype.append( math.floor(random.random() *
72 (self.imax - self.imin + 1)) + self.imin)
73 elif self.mode == 'float':
74 self.genotype.append( (random.random() * (self.imax - self.imin)) + self.imin)
75 elif self.mode == 'char':
76 self.genotype.append( self.alphabet[int(random.random() * len(self.alphabet)) ] )
77 else:
78 raise "unknownMode", self.mode
79
80 def copy(self):
81 return deepcopy(self)
82
83 def __getitem__(self, val):
84 return self.genotype[val]
85
86 def __len__(self):
87 return len(self.genotype)
88
89 def display(self):
90 if self.mode == 'bit' or self.mode == 'integer':
91 print string.join(map(lambda v: `int(v)`, self.genotype), "")
92 elif self.mode == 'float':
93 map(lambda v: display("%3.2f" % v), self.genotype)
94 elif self.mode == 'char':
95 print string.join(self.genotype, '')
96 else:
97 raise "unknownMode", self.mode
98
99 def mutate(self, mutationRate):
100 """
101 Depending on the mutationRate, will mutate the genotype.
102 """
103 for i in range(len(self.genotype)):
104 if flip(mutationRate):
105 if self.verbose > 2:
106 print "mutating at position", i
107 if self.mode == 'bit':
108 self.genotype[i] = not self.genotype[i]
109 elif self.mode == 'integer':
110 r = random.random()
111 if (r < .33):
112 self.genotype[i] += round(random.random() * self.maxStep)
113 self.genotype[i] = min(self.genotype[i], self.max)
114 elif (r < .67):
115 self.genotype[i] -= round(random.random() * self.maxStep)
116 self.genotype[i] = max(self.genotype[i], self.min)
117 else:
118 self.genotype[i] = round(random.random() * (self.max - self.min + 1)) + self.min
119 elif self.mode == 'float':
120 r = random.random()
121 if (r < .33):
122 self.genotype[i] += (random.random() * self.maxStep)
123 self.genotype[i] = min(self.genotype[i], self.max)
124 elif (r < .67):
125 self.genotype[i] -= (random.random() * self.maxStep)
126 self.genotype[i] = max(self.genotype[i], self.min)
127 else:
128 self.genotype[i] = (random.random() * (self.max - self.min)) + self.min
129 elif self.mode == 'char':
130 self.genotype[i] = self.alphabet[ int(random.random() * len(self.alphabet)) ]
131 else:
132 raise "unknownMode", self.mode
133
134 def crossover(self, parent2, crossoverRate):
135 """
136 Depending on the crossoverRate, will return two new children
137 created by crossing over the given parents at a single point,
138 or will return copies of the parents.
139 """
140 parent1 = self
141 geneLength = len(parent1.genotype)
142 if flip(crossoverRate):
143 p1 = parent1.genotype[:]
144 p2 = parent2.genotype[:]
145 child1 = [0] * geneLength
146 child2 = [0] * geneLength
147 # go through and pick the crossoverpoints:
148 if self.crossoverPoints == -3:
149 # one right in middle
150 crossPoints = [0] * geneLength
151 crossPoints[int(geneLength/2)] = 1
152 elif self.crossoverPoints == -2:
153 # no crossoverpoints; I know, this should be zero
154 crossPoints = [0] * geneLength
155 elif self.crossoverPoints == -1:
156 # shuffle: every other one
157 crossPoints = [1] * geneLength
158 elif self.crossoverPoints > 0:
159 # number of cross points:
160 # NOTE: not guaranteed to be exactly that many;
161 # could duplicate randomly
162 crossPoints = [0] * geneLength
163 for i in range(self.crossoverPoints):
164 crossPoints[(int)(random.random() * geneLength)] = 1
165 elif self.crossoverPoints == 0:
166 # uniform crossover when crossoverPoints = 0
167 crossPoints = [0] * geneLength
168 for i in range(geneLength):
169 # flip coin for each position:
170 if random.random() < .5:
171 crossPoints[i] = 1
172 else:
173 raise "unknownCrossoverType", self.crossoverPoints
174 # now, each time there is a cross point, swap parents
175 for i in range(geneLength):
176 if crossPoints[i]:
177 if self.verbose > 2:
178 print "crossing over at point", i
179 p1, p2 = p2, p1
180 child1[i] = p1[i]
181 child2[i] = p2[i]
182 new_child1 = self.__class__(**self.args)
183 new_child2 = self.__class__(**self.args)
184 new_child1.genotype = child1
185 new_child2.genotype = child2
186 return new_child1, new_child2
187 else:
188 if self.verbose > 2:
189 print "no crossover"
190 return parent1.copy(), parent2.copy()
191
192 class Population:
193 def __init__(self, cnt, geneConstructor, **args):
194 self.sumFitness = 0
195 self.avgFitness = 0
196 self.individuals = []
197 self.eliteMembers = []
198 self.elitePercent = 0.0
199 self.bestMember = -1
200 self.size = cnt
201 self.verbose = 0
202 self.args = args
203 self.geneConstructor = geneConstructor
204 if args.has_key('elitePercent'):
205 self.elitePercent = args['elitePercent']
206 if args.has_key('verbose'):
207 self.verbose = args['verbose']
208 for i in range(cnt):
209 self.individuals.append(geneConstructor(pos = i,
210 popSize = cnt,
211 **args))
212 def copy(self):
213 newPop = self.__class__(0, self.geneConstructor, **self.args)
214 newPop.size = self.size
215 for i in range(self.size):
216 newPop.individuals.append( self.individuals[i].copy() )
217 return newPop
218
219 def __getitem__(self, val):
220 return self.individuals[val]
221
222 def __len__(self):
223 return len(self.individuals)
224
225 def select(self):
226 """
227 Select a single individual via the roulette wheel method.
228 Algorithm from Goldberg's book, page 63. NOTE: fitness
229 function must return positive values to use this method
230 of selection.
231 """
232 index = 0
233 partsum = 0.0
234 if self.sumFitness == 0:
235 raise "Population has a total of zero fitness"
236 spin = random.random() * self.sumFitness
237 while index < self.size-1:
238 fitness = self.individuals[index].fitness
239 if fitness < 0:
240 raise "Negative fitness in select", fitness
241 partsum += self.individuals[index].fitness
242 if partsum >= spin:
243 break
244 index += 1
245 if self.verbose > 2:
246 print "selected",
247 self.individuals[index].display(),
248 print "fitness", self.individuals[index].fitness
249 return self.individuals[index].copy()
250
251 def statistics(self):
252 """
253 Maintains important statistics about the current population.
254 It calculates total fitness, average fitness, best fitness,
255 and worst fitness. Stores the best individual in the variable
256 self.bestMember. When the elitePercent is greater than zero,
257 this method also maintains a list of the elite members of the
258 population so that they can be saved for the next generation.
259 """
260 self.sumFitness = 0
261 best = self.individuals[0]
262 best.bestPosition = 0
263 worst= self.individuals[0]
264 self.eliteMembers = self.individuals[0:int(self.elitePercent * len(self.individuals))]
265 self.eliteMembers.sort(lambda x, y: cmp( x.fitness, y.fitness))
266 for i in range(self.size):
267 current = self.individuals[i]
268 current.position = i #needed to save the elite members of the population
269 self.sumFitness += current.fitness
270 if current.fitness < worst.fitness:
271 worst = current
272 if current.fitness > best.fitness:
273 best = current
274 best.bestPosition = i
275 if len(self.eliteMembers) > 0 and current.fitness > self.eliteMembers[0].fitness:
276 self.eliteMembers.append( current )
277 self.eliteMembers.sort(lambda x, y: cmp( x.fitness, y.fitness))
278 self.eliteMembers = self.eliteMembers[1:]
279 self.bestMember = best
280 self.avgFitness = (self.sumFitness * 1.0) / self.size
281 if self.verbose > 0:
282 print "Fitness: Total", "%7.2f" % self.sumFitness,
283 print "Best", "%5.2f" % best.fitness,
284 print "Average", "%5.2f" % self.avgFitness,
285 print "Worst", "%5.2f" % worst.fitness
286 print "Elite fitness:", map( lambda x: x.fitness, self.eliteMembers)
287 sys.stdout.flush()
288
289 class GA:
290 """
291 Class which defines everything needed to run a GA.
292 """
293 def __init__(self, population, **args):
294 self.averageLog = None
295 self.bestLog = None
296 self.mutationRate = 0.1
297 self.crossoverRate = 0.6
298 self.maxGeneration = 0
299 self.generation = 0
300 self.verbose = 0
301 if args.has_key('verbose'):
302 self.verbose = args['verbose']
303 if args.has_key('mutationRate'):
304 self.mutationRate = args['mutationRate']
305 if args.has_key('crossoverRate'):
306 self.crossoverRate = args['crossoverRate']
307 if args.has_key('maxGeneration'):
308 self.maxGeneration = args['maxGeneration']
309 x = random.random() * 100000 + time.time()
310 self.setSeed(x)
311 self.origPop = population
312 if self.verbose > 0:
313 print "crossoverRate = %.3f" % self.crossoverRate
314 print "mutationRate = %.3f" % self.mutationRate
315 print "populationSize = %d" % self.origPop.size
316 print "elitePercent = %.3f" % self.origPop.elitePercent
317 print "maxGeneration = %d" % self.maxGeneration
318 print "================================================================================"
319 self.setup(**args)
320 self.reInitialize()
321
322 def setup(self, **args):
323 pass
324
325 def reInitialize(self):
326 self.pop = self.origPop.copy()
327 self.initialize()
328
329 def initialize(self):
330 self.applyFitnessFunction()
331 if self.verbose > 0:
332 print "-" * 60
333 print "Initial population"
334 self.pop.statistics()
335 if self.verbose > 1:
336 self.display()
337
338 def logAverageFitness(self, filename="GAAvgFitness"):
339 self.averageLog = open(filename, 'w')
340
341 def logBestFitness(self, filename="GABestFitness"):
342 self.bestLog = open(filename, 'w')
343
344 def isDone(self):
345 # Override this
346 pass
347
348 def fitnessFunction(self, genePosition, **args):
349 # Override this
350 pass
351
352 def applyFitnessFunction(self):
353 for i in range( len(self.pop.individuals) ):
354 self.pop.individuals[i].fitness = self.fitnessFunction(i)
355
356 def setSeed(self, value):
357 self.seed = value
358 random.seed(self.seed)
359
360 def display_one(self, p):
361 self.pop.individuals[p].display()
362 print "Fitness:", self.pop.individuals[p].fitness
363
364 def display(self):
365 print "Population:"
366 for p in range(len(self.pop.individuals)):
367 self.display_one(p)
368
369 def generate(self):
370 """
371 Iteratively creates a new population from the current population.
372 Selects two parents, attempts to cross them, and then attempts to
373 mutate the resulting children. The probability of these operations
374 occurring is determined by the crossoverRate and the mutationRate.
375 Overwrites the old population with the new population.
376 """
377 newpop = range(self.pop.size)
378 i = 0
379 while i < self.pop.size - 1:
380 parent1 = self.pop.select()
381 parent2 = self.pop.select()
382 newpop[i], newpop[i+1] = parent1.crossover(parent2, self.crossoverRate)
383 newpop[i].mutate(self.mutationRate)
384 newpop[i+1].mutate(self.mutationRate)
385 i += 2
386 # For odd sized populations, need to create the last child
387 if self.pop.size % 2 == 1:
388 newpop[self.pop.size-1] = self.pop.select()
389 newpop[self.pop.size-1].mutate(self.mutationRate)
390 # Copy new generation into population
391 elitePositions = map( lambda x: x.position, self.pop.eliteMembers)
392 for i in range(self.pop.size):
393 if i not in elitePositions:
394 self.pop.individuals[i] = newpop[i]
395
396 def evolve(self, cont = 0):
397 if not cont:
398 self.generation = 0
399 else:
400 if self.generation == self.maxGeneration:
401 self.maxGeneration = self.generation + 100
402 while self.generation < self.maxGeneration or self.maxGeneration == 0:
403 self.generation += 1
404 if self.verbose > 0:
405 print "-" * 60
406 print "Generation", self.generation
407 self.generate()
408 self.applyFitnessFunction()
409 self.pop.statistics()
410 if self.bestLog != None:
411 self.bestLog.write("%d %5.2f\n" %
412 (self.generation,
413 self.pop.bestMember.fitness))
414 if self.averageLog != None:
415 self.averageLog.write("%d %5.2f\n" %
416 (self.generation,
417 self.pop.avgFitness))
418 if self.verbose > 1:
419 self.display()
420 if self.isDone():
421 break
422 print "-" * 60
423 print "Done evolving at generation", self.generation
424 print "Current best individual [#%d]" % self.pop.bestMember.bestPosition,
425 self.pop.bestMember.display()
426 print "Fitness", self.pop.bestMember.fitness
427
428 def saveToFile(self, filename):
429 import pickle
430 fp = open(filename, "w")
431 if self.verbose > 0:
432 print "Saving GA to '%s'..." % (filename,)
433 pickle.dump(self, fp)
434 fp.close()
435
436 def loadFromFile(self, filename):
437 # probably just copy this... no need to create an entire object
438 # to load another one.
439 import pickle
440 fp = open(filename, "w")
441 if self.verbose > 0:
442 print "Loading GA from '%s'..." % (filename,)
443 fp.close()
444 return pickle.load(fp)
445
446 def saveGenesToFile(self, filename, listOfPositions = None):
447 import pickle
448 if listOfPositions == None:
449 listOfPositions = range(len(self.pop.individuals))
450 fp = open(filename, "w")
451 if self.verbose > 0:
452 print "Saving %d genes to '%s'..." % (len(listOfPositions), filename)
453 pickle.dump( len(listOfPositions), fp)
454 for i in listOfPositions:
455 pickle.dump(self.pop.individuals[i], fp)
456 fp.close()
457
458 def getGenesFromFile(self, filename):
459 import pickle
460 fp = open(filename, "r")
461 geneCount = pickle.load(fp)
462 if self.verbose > 0:
463 print "Loading %d genes from '%s'..." % (geneCount, filename)
464 individuals = []
465 for i in range(geneCount):
466 individuals.append(pickle.load(fp))
467 fp.close()
468 return individuals
469
470 def loadGenesFromFile(self, filename):
471 self.pop.individuals = self.getGenesFromFile(filename)
472
473 def initGenesFromFile(self, filename, sampleSize = 0,mutate = 1,full = 0):
474 # sampleSize = how many to get from saved pop?
475 # mutate = should I mutate them?
476 # full = should I create a full pop, or just replace sampleSize?
477 oldGenes = self.getGenesFromFile(filename)
478 if sampleSize == 0:
479 sampleSize = len(oldGenes)
480 if self.verbose > 0:
481 print "oldGenes had %d individuals" % len(oldGenes)
482 print "current has %d individuals" % len(self.pop.individuals)
483 print "Loading %d..." % sampleSize
484 if full:
485 currentOld = 0
486 for i in range(len(self.pop.individuals)):
487 currentOld = currentOld % len(oldGenes)
488 self.pop.individuals[i] = oldGenes[currentOld]
489 currentOld += 1
490 if mutate:
491 self.pop.individuals[i].mutate(self.mutationRate)
492 else:
493 for i in range(sampleSize):
494 self.pop.individuals[i] = oldGenes[i]
495 if mutate:
496 self.pop.individuals[i].mutate(self.mutationRate)
497
498 if __name__ == '__main__':
499 # Here is a test to evolve a list of integers to maximize their sum:
500
501 class MaxSumGA(GA):
502 def fitnessFunction(self, i):
503 return max(sum(self.pop.individuals[i].genotype), 0)
504 def isDone(self):
505 print "Best:",
506 self.pop.bestMember.display()
507 print
508 return self.pop.bestMember.fitness > 30
509
510 print "Do you want to evolve a list of integers to maximize their sum? ",
511 if sys.stdin.readline().lower()[0] == 'y':
512 print
513 ga = MaxSumGA(Population(20, Gene, size=10, mode='integer',
514 verbose=1, elitePercent = .1,
515 max = 3, maxStep = 2, min = 0,
516 crossoverPoints = 1),
517 mutationRate=0.1, crossoverRate=0.5, verbose=1,
518 maxGeneration=50)
519 ga.evolve()
520 print "Testing loading/saving..."
521 ga.saveGenesToFile("maxsumga.genes")
522 print "Deleting genes..."
523 ga.pop.individuals = []
524 ga.loadGenesFromFile("maxsumga.genes")
525 print "Press enter to continue evolving...",
526 sys.stdin.readline()
527 ga.evolve()
528 print "Press enter to Test init from file (load all with mutate)...",
529 sys.stdin.readline()
530 print "reInitialize pop..."
531 ga.reInitialize()
532 ga.initGenesFromFile("maxsumga.genes")
533 ga.evolve()
534 print "Press enter to Test init from file (load 1 no mutate)...",
535 sys.stdin.readline()
536 ga.saveGenesToFile("bestsumga.genes", (ga.pop.bestMember.position,))
537 ga.reInitialize()
538 ga.initGenesFromFile("bestsumga.genes", 1, 0)
539 ga.evolve()
540 print "Press enter to Test init from file (load 1, with mutate, full)...",
541 sys.stdin.readline()
542 ga.reInitialize()
543 ga.initGenesFromFile("bestsumga.genes", mutate = 1, full = 1)
544 ga.evolve()
545 print
546
547 # Here is a test to evolve the weights/biases in a neural network
548 # that solves the XOR problem:
549
550 from pyrobot.brain.conx import *
551 class NNGA(GA):
552 def __init__(self, cnt):
553 n = Network()
554 n.add( Layer('input', 2) )
555 n.add( Layer('hidden', 3) )
556 n.add( Layer('output', 1) )
557 n.connect('input', 'hidden')
558 n.connect('hidden', 'output')
559 n.setInputs([[0.0, 0.0],
560 [0.0, 1.0],
561 [1.0, 0.0],
562 [1.0, 1.0]])
563 n.setOutputs([[0.0],
564 [1.0],
565 [1.0],
566 [0.0]])
567 n.setVerbosity(0)
568 n.setTolerance(.4)
569 n.setLearning(0)
570 g = n.arrayify()
571 self.network = n
572 GA.__init__(self,
573 Population(cnt, Gene, size=len(g), verbose=1,
574 min=-10, max=10, maxStep = 1,
575 imin=-10, imax=10,
576 elitePercent = .01),
577 mutationRate=0.05, crossoverRate=0.6,
578 maxGeneration=400, verbose=1)
579 def fitnessFunction(self, genePos):
580 self.network.unArrayify(self.pop.individuals[genePos].genotype)
581 error, correct, count, pcorrect = self.network.sweep()
582 return 4 - error
583 def isDone(self):
584 self.network.unArrayify(self.pop.bestMember.genotype)
585 error, correct, count, pcorrect = self.network.sweep()
586 print "Correct:", correct
587 return correct == 4
588
589 print "Do you want to evolve a neural network that can do XOR? ",
590 if sys.stdin.readline().lower()[0] == 'y':
591 ga = NNGA(300)
592 ga.evolve()
593 ga.network.unArrayify(ga.pop.bestMember.genotype)
594 ga.network.setInteractive(1)
595 ga.network.sweep()
596 ga.saveGenesToFile("gann.pop")
597 ga.initGenesFromFile("gann.pop")
598
599 print "Do you want to evolve a phrase? ",
600 if sys.stdin.readline().lower()[0] == 'y':
601 phrase = "evolution is one cool search mechanism"
602 size = len(phrase)
603 print
604 class PhraseGA(GA):
605 def fitnessFunction(self, i):
606 sum = 0
607 for c in range(len(self.pop.individuals[i].genotype)):
608 if self.pop.individuals[i].genotype[c] == phrase[c]:
609 sum += 1
610 return float(sum) / len(self.pop.individuals[i].genotype)
611 def isDone(self):
612 print "Best:",
613 self.pop.bestMember.display()
614 return (phrase == string.join(self.pop.bestMember.genotype, ""))
615
616 ga = PhraseGA(Population(300, Gene, size=size, mode='char',
617 verbose=1, elitePercent = .1,
618 crossoverPoints = 2),
619 mutationRate=0.06, crossoverRate=0.6, verbose=1,
620 maxGeneration=0)
621 ga.evolve()
622
623 print "Do you want to play mastermind? ",
624 if sys.stdin.readline().lower()[0] == 'y':
625 # composed of N colors, M places (usually 6 and 4)
626 # feedback is # in correct place, # of correct color
627 phrase = "abcdefghijklmnopqrstuvwxyz"
628 size = len(phrase)
629 primer = 0
630 print
631 class MasterMindGA(GA):
632 def fitnessFunction(self, i):
633 sumPosition = 0
634 sumColor = 0
635 guessed = []
636 correct = []
637 for c in range(len(self.pop.individuals[i].genotype)):
638 if self.pop.individuals[i].genotype[c] == phrase[c]:
639 sumPosition += 1
640 else:
641 guessed.append(self.pop.individuals[i].genotype[c])
642 correct.append(phrase[c])
643 for g in guessed:
644 if g in correct:
645 correct.remove(g)
646 sumColor += 1
647 if primer:
648 if (sumColor + sumPosition > size/2):
649 goodPosition = 10
650 goodColor = 1
651 else:
652 goodPosition = 0
653 goodColor = 1
654 else:
655 goodPosition = 100
656 goodColor = 1
657 return sumColor * goodColor + sumPosition * goodPosition
658 def isDone(self):
659 print "Best:",
660 self.pop.bestMember.display()
661 return (phrase == string.join(self.pop.bestMember.genotype, ""))
662
663 ga = MasterMindGA(Population(300, Gene, size=size, mode='char',
664 verbose=1, elitePercent = .1,
665 crossoverPoints = 2),
666 mutationRate=0.06, crossoverRate=0.6, verbose=1,
667 maxGeneration=0)
668 ga.evolve()
669
670 # 26 ** 26 = 6,156,119,580,207,157,310,796,674,288,400,203,776 6x10^36
671 # 60 * 300 = 18,000
672 # 224 * 300 = 67200
673
674 # generations
675 # 224
676 # 171
677 # 60
678 # 167
679 # 404
680
681 # With priming
682 # 73
683 # 126
684 # 260
685
686 # no overlap
687 # 98
688 # 158

Properties

Name Value
svn:eol-style native
svn:executable *
svn:keywords Author Date Id Revision

dblank@brynmawr.edu
ViewVC Help
Powered by ViewVC 1.1.3