1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 '''
35 This module contains a basic framework for a genetic algorithm. This specific
36 implentation specifically keeps high selective pressure on the population by
37 always keeping the best individuals found. The algorithm checks to make sure
38 that no individual is tested twice.
39
40 Chromozomes are constructed from "Variable" classes that govern rondom
41 generation and recombination of each element in the chromozome. For
42 example if your fitness function requires a number from one to ten you
43 could uses a ChoiceVariable with choices = [1,2,3,4,5,6,7,8,9,10].
44
45 TODO: Add a tutorial.
46
47 This class also has some examples in the unit tests.
48 '''
49
50 import random
51
52 import math
53 import multiprocessing as mp
54
55 import traceback
56 import numpy as np
57 import copy
58
59 import pyvision as pv
60
61
62 _GA_EVALUATE="GA_EVALUATE"
63 _GA_STOP_WORKER="GA_STOP_WORKER"
64 _GA_SCORE = "GA_SCORE"
65
67 minval,maxval = min(minval,maxval),max(minval,maxval)
68 val = min(val,maxval)
69 val = max(val,minval)
70 return val
71
72
74 minval,maxval = min(minval,maxval),max(minval,maxval)
75 dist = maxval - minval
76 if val > maxval:
77 t1 = (val - maxval)/dist
78 t2 = t1 - math.floor(t1)
79 t3 = minval + dist*t2
80 return t3
81 elif val < minval:
82 t1 = (val - minval)/dist
83 t2 = t1 - math.ceil(t1)
84 t3 = maxval + dist*t2
85 return t3
86
87 return val
88
89
91 '''
92 This is a superclass for a variable that is optimized by the GA. It
93 has three methods that need to be overridden by subclasses: combine,
94 mutate, and generate.
95
96
97 '''
99 self.mutation_rate = mutation_rate
100
102 ''' Initialize this variable randomly '''
103 raise NotImplementedError("This method should be overridden by subclasses")
104
106 '''combine this variable with other.'''
107 raise NotImplementedError("This method should be overridden by subclasses")
108
110 '''introduce mutations into the variable.'''
111 raise NotImplementedError("This method should be overridden by subclasses")
112
114 '''generate the actual value that will be populated in the arguments'''
115 raise NotImplementedError("This method should be overridden by subclasses")
116
119
121 return str(self.value)
122
123
124
126
127 - def __init__(self,minval,maxval,**kwargs):
128 GAVariable.__init__(self, **kwargs)
129 self.minval,self.maxval = min(minval,maxval),max(minval,maxval)
130 self.random()
131
133 self.value = _clipRange(self.value, self.minval, self.maxval)
134
136 ''' Initialize this variable randomly '''
137 self.value = self.minval + (self.maxval - self.minval)*random.random()
138 self.clipRange()
139
141 '''combine this variable with other.'''
142 dist = np.abs(self.value - other.value)+0.000001
143 if random.randint(0,1) == 0:
144 self.value = other.value
145 self.value += np.random.normal(0,dist/3.0)
146 self.clipRange()
147
149 '''introduce mutations into the variable.'''
150 if random.random() < self.mutation_rate:
151 dist = np.abs(self.maxval-self.minval)
152 self.value += np.random.normal(0,dist/50.0)
153 self.clipRange()
154
156 '''generate the actual value that will be populated in the arguments'''
157 return self.value
158
160 return str(self.value)
161
162
164 '''
165 A float on a log scale.
166 '''
167
168 - def __init__(self,minval,maxval,**kwargs):
169 GAVariable.__init__(self, **kwargs)
170 minval = np.log(minval)
171 maxval = np.log(maxval)
172 self.minval,self.maxval = min(minval,maxval),max(minval,maxval)
173 self.random()
174
176 self.value = _clipRange(self.value, self.minval, self.maxval)
177
179 ''' Initialize this variable randomly '''
180 self.value = self.minval + (self.maxval - self.minval)*random.random()
181 self.clipRange()
182
184 '''combine this variable with other.'''
185 dist = np.abs(self.value - other.value)+0.000001
186 if random.randint(0,1) == 0:
187 self.value = other.value
188 self.value += np.random.normal(0,dist/3.0)
189 self.clipRange()
190
192 '''introduce mutations into the variable.'''
193 if random.random() < self.mutation_rate:
194 dist = np.abs(self.maxval-self.minval)
195 self.value += np.random.normal(0,dist/50.0)
196 self.clipRange()
197
199 '''generate the actual value that will be populated in the arguments'''
200 return np.exp(self.value)
201
203 return str(np.exp(self.value))
204
205
207 '''
208 Maps to an angle in the range -pi to pi.
209 '''
210
211 - def __init__(self,minval=-np.pi,maxval=np.pi,**kwargs):
215
218
220 ''' Initialize this variable randomly '''
221 self.value = self.minval + (self.maxval - self.minval)*random.random()
222 self.clipRange()
223
225 '''combine this variable with other.'''
226 t1 = 0.5*(self.minval + self.maxval)
227 t2 = t1 - self.value
228 t3 = _circularRange( other.value + t2, self.minval, self.maxval)
229 dist = np.abs(t3 - t1)
230
231
232 if random.randint(0,1) == 0:
233 self.value = other.value
234
235
236 self.value += np.random.normal(0,dist/3.0)
237
238
239 self.clipRange()
240
242 '''introduce mutations into the variable.'''
243 if random.random() < self.mutation_rate:
244 dist = np.abs(self.maxval-self.minval)
245 self.value += np.random.normal(0,dist/50.0)
246 self.clipRange()
247
249 '''generate the actual value that will be populated in the arguments'''
250 return self.value
251
253 return str(self.value)
254
256
257 - def __init__(self,min_width=0.2,max_width=1.0,min_height=0.2,max_height=1.0,**kwargs):
258 GAVariable.__init__(self,**kwargs)
259
260 assert min_width >= 0
261 assert min_width <= 1
262 assert max_width >= 0
263 assert max_width <= 1
264 assert min_height >= 0
265 assert min_height <= 1
266 assert max_height >= 0
267 assert max_height <= 1
268 assert min_width <= max_width
269 assert min_height <= max_height
270
271 self.min_width = min_width
272 self.max_width = max_width
273 self.min_height = min_height
274 self.max_height = max_height
275
276 self.random()
277
279 self.width = _clipRange(self.width, self.min_width, self.max_width)
280 self.height = _clipRange(self.height, self.min_height, self.max_height)
281 self.cx = _clipRange(self.cx, 0.5*self.width, 1.0 - 0.5*self.width)
282 self.cy = _clipRange(self.cy, 0.5*self.height, 1.0 - 0.5*self.height)
283
284
286 ''' Initialize this variable randomly '''
287 self.cx = random.random()
288 self.cy = random.random()
289
290 diff = self.max_width - self.min_width
291 self.width = self.min_width + random.random() * diff
292
293 diff = self.max_height - self.min_height
294 self.height = self.min_height + random.random() * diff
295 self.clipRange()
296
297
299 '''combine this variable with other.'''
300
301
302 cx_dist = np.abs(self.cx - other.cx) + 1e-7
303 cy_dist = np.abs(self.cy - other.cy) + 1e-7
304 w_dist = np.abs(self.width - other.width) + 1e-7
305 h_dist = np.abs(self.height - other.height) + 1e-7
306
307 if random.randint(0,1) == 0:
308 self.cx = other.cx
309 self.cy = other.cy
310 self.width = other.width
311 self.height = other.height
312
313 if cx_dist <= 0 or cy_dist <= 0 or w_dist <= 0 or h_dist <= 0 :
314 print "Combining:",self,other,cx_dist,cy_dist,w_dist,h_dist
315 self.cx += np.random.normal(0,cx_dist/3.0)
316 self.cy += np.random.normal(0,cy_dist/3.0)
317 self.width += np.random.normal(0,w_dist/3.0)
318 self.height += np.random.normal(0,h_dist/3.0)
319
320
321 self.clipRange()
322
324 '''introduce mutations into the variable.'''
325 if random.random() < self.mutation_rate:
326 dist = np.abs(1.0)
327 self.cx += np.random.normal(0,dist/50.0)
328
329 dist = np.abs(1.0)
330 self.cy += np.random.normal(0,dist/50.0)
331
332 dist = np.abs(1.0)
333 self.cy += np.random.normal(0,dist/50.0)
334
335 dist = np.abs(1.0)
336 self.cy += np.random.normal(0,dist/50.0)
337
338 self.clipRange()
339
341 '''generate the actual value that will be populated in the arguments'''
342 return pv.CenteredRect(self.cx, self.cy, self.width, self.height)
343
346
349
350
351
353
354 - def __init__(self,min_width=0.2,max_width=1.0,min_height=0.2,max_height=1.0,**kwargs):
355 GAVariable.__init__(self,**kwargs)
356
357 assert min_width >= 0
358 assert min_width <= 1
359 assert max_width >= 0
360 assert max_width <= 1
361 assert min_height >= 0
362 assert min_height <= 1
363 assert max_height >= 0
364 assert max_height <= 1
365 assert min_width <= max_width
366 assert min_height <= max_height
367
368 self.min_width = min_width
369 self.max_width = max_width
370 self.min_height = min_height
371 self.max_height = max_height
372
373 self.random()
374
376 self.left = _clipRange(0, self.left, 1)
377 self.right = _clipRange(0, self.right, 1)
378 self.top = _clipRange(0, self.top, 1)
379 self.bottom = _clipRange(0, self.bottom,1)
380 if self.left > self.right:
381 self.left,self.right = self.right,self.left
382 if self.bottom < self.top:
383 self.bottom,self.top = self.top,self.bottom
384
385
393
394
396 '''combine this variable with other.'''
397
398
399 l_dist = np.abs(self.left - other.left) + 1e-7
400 r_dist = np.abs(self.right - other.right) + 1e-7
401 t_dist = np.abs(self.top - other.top) + 1e-7
402 b_dist = np.abs(self.bottom - other.bottom) + 1e-7
403
404 if random.randint(0,1) == 0:
405 self.left = other.left
406 self.right = other.right
407 self.top = other.top
408 self.bottom = other.bottom
409
410 if l_dist <= 0 or r_dist <= 0 or t_dist <= 0 or b_dist <= 0 :
411 print "Combining:",self,other,l_dist,r_dist,t_dist,b_dist
412 self.left += np.random.normal(0,l_dist/3.0)
413 self.right += np.random.normal(0,r_dist/3.0)
414 self.top += np.random.normal(0,t_dist/3.0)
415 self.bottom += np.random.normal(0,b_dist/3.0)
416
417
418 self.clipRange()
419
421 '''introduce mutations into the variable.'''
422 if random.random() < self.mutation_rate:
423 dist = np.abs(1.0)
424 self.left += np.random.normal(0,dist/50.0)
425
426 dist = np.abs(1.0)
427 self.right += np.random.normal(0,dist/50.0)
428
429 dist = np.abs(1.0)
430 self.top += np.random.normal(0,dist/50.0)
431
432 dist = np.abs(1.0)
433 self.bottom += np.random.normal(0,dist/50.0)
434
435 self.clipRange()
436
438 '''generate the actual value that will be populated in the arguments'''
439 return pv.BoundingRect([pv.Point(self.left,self.top),pv.Point(self.right,self.bottom)])
440
443
446
447
448
449
451
452 - def __init__(self,minval,maxval,**kwargs):
453 GAVariable.__init__(self, **kwargs)
454 self.minval,self.maxval = min(minval,maxval),max(minval,maxval)
455 self.random()
456
458 self.value = _clipRange(self.value, self.minval, self.maxval)
459
461 ''' Initialize this variable randomly '''
462 self.value = random.randint(self.minval,self.maxval)
463 self.clipRange()
464
466 '''combine this variable with other.'''
467 dist = np.abs(self.value - other.value)
468 if random.randint(0,1) == 0:
469 self.value = other.value
470 self.value += random.randint(-dist-1,dist+1)
471 self.clipRange()
472
474 '''introduce mutations into the variable.'''
475 if random.random() < self.mutation_rate:
476 dist = np.abs(self.maxval-self.minval)
477 self.value += random.randint(-dist-1,dist+1)
478 self.clipRange()
479
481 '''generate the actual value that will be populated in the arguments'''
482 return self.value
483
485 return str(self.value)
486
487
489
493
494
496 ''' Initialize this variable randomly '''
497 self.value = random.randint(0,1) == 1
498
500 '''combine this variable with other.'''
501 if random.randint(0,1) == 0:
502 self.value = other.value
503
505 '''introduce mutations into the variable.'''
506 if random.random() < self.mutation_rate:
507 self.random()
508
510 '''generate the actual value that will be populated in the arguments'''
511 return self.value
512
514 return str(self.value)
515
516
518
523
524
526 ''' Initialize this variable randomly '''
527 self.value = random.sample(self.choices,1)[0]
528
530 '''combine this variable with other.'''
531 if random.randint(0,1) == 0:
532 self.value = other.value
533
535 '''introduce mutations into the variable.'''
536 if random.random() < self.mutation_rate:
537 self.value = random.sample(self.choices,1)[0]
538
540 '''generate the actual value that will be populated in the arguments'''
541 return self.value
542
544 return str(self.value)
545
546
548
549 - def __init__(self,n_elements,**kwargs):
554
556 ''' Initialize this variable randomly '''
557 random.shuffle(self.ranking)
558
560 '''combine this variable with other.'''
561 a = [ (i,self.ranking[i]) for i in range(len(self.ranking))]
562 b = [ (i,other.ranking[i]) for i in range(len(self.ranking))]
563
564 a.sort(lambda x,y: cmp(x[1],y[1]))
565 b.sort(lambda x,y: cmp(x[1],y[1]))
566
567 c = []
568 for i in range(len(a)):
569 if random.randint(0,1) == 0:
570 c.append(a[i])
571 else:
572 c.append(b[i])
573
574 random.shuffle(c)
575
576 c.sort(lambda x,y: cmp(x[0],y[0]))
577
578 self.ranking = [r for _,r in c]
579
580
582 '''introduce mutations into the variable.'''
583 n = int(self.mutation_rate * self.n_elements) + 1
584 for _ in range(n):
585 i1 = random.randint(0,self.n_elements-1)
586 i2 = random.randint(0,self.n_elements-1)
587 x = self.ranking[i1]
588 del self.ranking[i1]
589 self.ranking.insert(i2,x)
590
591
593 '''generate the actual value that will be populated in the arguments'''
594 return self.ranking
595
598
600 return str(self.ranking)
601
602
604 ''' A vector constrained to length 1.0. '''
605 - def __init__(self,n_elements,**kwargs):
609
611 self.value = pv.unit(self.value)
612
614 ''' Initialize this variable randomly '''
615 self.value = np.random.normal(size=[self.n_elements])
616 self.clipRange()
617
619 '''combine this variable with other.'''
620 for i in range(len(self.value)):
621 dist = np.abs(self.value[i] - other.value[i])+0.000001
622 if random.randint(0,1) == 0:
623 self.value[i] = other.value[i]
624 self.value[i] += np.random.normal(0,dist/3.0)
625 self.clipRange()
626
627
628
630 '''introduce mutations into the variable.'''
631 if random.random() < self.mutation_rate:
632 for i in range(len(self.value)):
633 self.value[i] += np.random.normal(0,0.02)
634 self.clipRange()
635
636
638 '''generate the actual value that will be populated in the arguments'''
639 return self.value
640
642 return list(self.value.flatten())
643
645 return str(self.value)
646
648 ''' A positive vector that sums to 1.0. '''
649
650 - def __init__(self,n_elements,**kwargs):
654
656 self.value = self.value*(self.value > 0)
657 weight = self.value.sum()
658
659
660 weight = max(weight,0.0001)
661
662 self.value = self.value/weight
663
664
666 ''' Initialize this variable randomly '''
667 self.value = np.random.random(size=[self.n_elements])
668 self.clipRange()
669
671 '''combine this variable with other.'''
672 for i in range(len(self.value)):
673 dist = np.abs(self.value[i] - other.value[i])+0.000001
674 if random.randint(0,1) == 0:
675 self.value[i] = other.value[i]
676 self.value[i] += np.random.normal(0,dist/3.0)
677 self.clipRange()
678
680 '''introduce mutations into the variable.'''
681 if random.random() < self.mutation_rate:
682 for i in range(len(self.value)):
683 self.value[i] += np.random.normal(0,1)/(50.0*self.n_elements)
684 self.clipRange()
685
686
688 '''generate the actual value that will be populated in the arguments'''
689 return self.value
690
692 return list(self.value.flatten())
693
695 return str(self.value)
696
697
701
705
706
707
717
719 for i in args.keys():
720 if isinstance(args[i],GAVariable):
721 args[i] = args[i].generate()
722 elif isinstance(args[i],(list,tuple)):
723 list_generate(args[i])
724 elif isinstance(args[i],dict):
725 dict_generate(args[i])
726
727
729 '''
730 This is a work function that gets called on child processes
731 to evaluate a fitness function.
732 '''
733 try:
734 fitness,args,kwargs = data
735 assert isinstance(args, (list,tuple))
736 assert isinstance(kwargs, dict)
737 args = copy.deepcopy(args)
738 kwargs = copy.deepcopy(kwargs)
739 list_generate(args)
740 dict_generate(kwargs)
741 score = fitness(*args,**kwargs)
742 except:
743 print "Error in work queue."
744 traceback.print_exc()
745 score = np.inf
746 return score
747
748
750
751 - def __init__(self,fitness,args=[],kwargs={},initializer=None,initargs=[],population_size=100,n_processes="AUTO"):
752 self.fitness = fitness
753 self.args = args
754 self.kwargs = kwargs
755 self.initializer = initializer
756 self.initargs=initargs
757 self.population_size = population_size
758 self.n_processes = n_processes
759 if self.n_processes == "AUTO":
760 self.n_processes = mp.cpu_count()
761
762 self.run_data = None
763
764 self.running_workers = 0
765
766 self.best_score = np.inf
767 self.population = []
768 self.bests = []
769 self.worsts = []
770 self.history = []
771 self.iter = 0
772
781
790
791
793 '''
794 Randomly generate an individual for initialization
795 '''
796 args = copy.deepcopy(self.args)
797 kwargs = copy.deepcopy(self.kwargs)
798
799 self.list_random(args)
800 self.dict_random(kwargs)
801
802 return args,kwargs
803
804
813
822
823
825 '''
826 Randomly generate an individual for initialization
827 '''
828 args = copy.deepcopy(args)
829
830 if isinstance(args, (list,tuple)):
831 self.list_mutate(args)
832 if isinstance(args, dict):
833 self.dict_mutate(args)
834
835 return args
836
837
850
852 assert len(args) == len(other)
853
854 for key in args.keys():
855 if isinstance(args[key],GAVariable):
856 args[key].combine(other[key])
857 elif isinstance(args[key],(list,tuple)):
858 self.list_combine(args[key], other[key])
859 elif isinstance(args[key],dict):
860 self.dict_combine(args[key], other[key])
861 else:
862 pass
863
875
876
878
879
880 extras = []
881 try:
882 extras = score[1:]
883 score = score[0]
884 except:
885
886 pass
887
888 if not np.isfinite(score):
889 return
890
891 if score < self.best_score:
892 self.best_score = score
893 if ilog != None:
894
895 print "New Best Score:",score
896
897 for i in range(len(args)):
898 print " arg%02d"%i,args[i]
899 keys = list(kwargs.keys())
900 keys.sort()
901 for key in keys:
902 print " %10s:"%key,kwargs[key]
903
904 self.population.append([score,args,kwargs])
905 self.population.sort(lambda x,y: cmp(x[0],y[0]))
906 self.population = self.population[:self.population_size]
907
908 self.history.append(score)
909 self.bests.append(self.population[0][0])
910 self.worsts.append(self.population[-1][0])
911
912 self.iter += 1
913
914 if ilog != None:
915 self.printPopulation()
916
917 ilog.pickle([score,args,kwargs],"Fitness_%0.8f"%score)
918 for i in xrange(len(extras)):
919 extra = extras[i]
920 if isinstance(extra,pv.Image):
921 ilog(extra,"extra_%02d_%0.8f"%(i,score))
922 else:
923 ilog.pickle(extra,"extra_%02d_%0.8f"%(i,score))
924
925 if self.iter % 64 == 0:
926 plot = pv.Plot(title="Population Statistics",xlabel="Iteration",ylabel="Score")
927 data = [ [i,self.bests[i]] for i in range(len(self.bests)) ]
928 plot.lines(data,width=3,color='green')
929 data = [ [i,self.history[i]] for i in range(len(self.bests)) ]
930 plot.points(data,shape=16,color='blue',size=2)
931 data = [ [i,self.worsts[i]] for i in range(len(self.bests)) ]
932 plot.lines(data,width=3,color='red')
933 ilog(plot,"PopulationData")
934
935
937 print "GA Population (Iteration %d):"%self.iter,
938 for i in range(len(self.population)):
939 if i % 10 == 0:
940 print
941 print " ",
942 print "%8.3f"%self.population[i][0],
943 print
944 print
945
946
947 - def optimize(self,max_iter=1000,callback=None,ilog=None,restart_dir=None):
948 '''
949 @returns: best_score, args, kwargs
950 '''
951
952
953 if self.n_processes > 1:
954 print "Init Params (%d cores):"%self.n_processes,self.initializer, self.initargs
955 pool = mp.Pool(self.n_processes,initializer=self.initializer,initargs=self.initargs)
956
957
958 work = []
959 for i in range(max(self.population_size-len(self.population),0)):
960 args,kwargs = self.random()
961 work.append((self.fitness,args,kwargs))
962
963 if self.n_processes > 1:
964 scores = pool.map(_gaWork, work)
965 for i in range(len(scores)):
966 score = scores[i]
967 _,args,kwargs = work[i]
968 self.addIndividual(score,args,kwargs,ilog=ilog)
969 else:
970 for each in work:
971 score = _gaWork(each)
972 _,args,kwargs = each
973 self.addIndividual(score,args,kwargs,ilog=ilog)
974
975 if len(self.population) < 2:
976 raise ValueError("Could not initialize population.")
977
978
979 while self.iter < max_iter:
980
981
982 n_work = max(1,self.n_processes)
983 work = []
984 while len(work) < n_work:
985 i1 = random.randint(0,len(self.population)-1)
986 i2 = random.randint(0,len(self.population)-1)
987 if i1 != i2:
988 _,args1,kwargs1 = self.population[i1]
989 _,args2,kwargs2 = self.population[i2]
990 args = self.combine(args1, args2)
991 kwargs = self.combine(kwargs1, kwargs2)
992 args = self.mutate(args)
993 kwargs = self.mutate(kwargs)
994 work.append((self.fitness,args,kwargs))
995
996 if self.n_processes > 1:
997 scores = pool.map(_gaWork, work)
998 for i in range(len(scores)):
999 score = scores[i]
1000 _,args,kwargs = work[i]
1001 self.addIndividual(score,args,kwargs,ilog=ilog)
1002 else:
1003 for each in work:
1004 score = _gaWork(each)
1005 _,args,kwargs = each
1006 self.addIndividual(score,args,kwargs,ilog=ilog)
1007
1008 if callback != None:
1009 callback(self.population)
1010
1011 args = copy.deepcopy(self.population[0][1])
1012 kwargs = copy.deepcopy(self.population[0][2])
1013 list_generate(args)
1014 dict_generate(kwargs)
1015
1016 return self.population[0][0],args,kwargs
1017