9
9
ann_evolve.py [-h] [--generations GENERATIONS] [--population POPULATION]
10
10
[--mu MUTATION_RATE] [--bottleneck BOTTLENECK] [--processes PROCESSORS]
11
11
[--output OUTPUT_FILE] [--objective OBJECTIVE] [--repetitions REPETITIONS]
12
- [--noise NOISE] [--nmoran NMORAN]
12
+ [--turns TURNS] [-- noise NOISE] [--nmoran NMORAN]
13
13
[--features FEATURES] [--hidden HIDDEN] [--mu_distance DISTANCE]
14
14
15
15
Options:
22
22
--output OUTPUT_FILE File to write data to [default: ann_weights.csv]
23
23
--objective OBJECTIVE Objective function [default: score]
24
24
--repetitions REPETITIONS Repetitions in objective [default: 100]
25
+ --turns TURNS Turns in each match [default: 200]
25
26
--noise NOISE Match noise [default: 0.00]
26
27
--nmoran NMORAN Moran Population Size, if Moran objective [default: 4]
27
28
--features FEATURES Number of ANN features [default: 17]
28
29
--hidden HIDDEN Number of hidden nodes [default: 10]
29
30
--mu_distance DISTANCE Delta max for weights updates [default: 5]
30
31
"""
31
32
32
- from itertools import repeat
33
- from multiprocessing import Pool
34
- from operator import itemgetter
35
33
import random
36
- from statistics import mean , pstdev
37
34
38
35
from docopt import docopt
39
36
import numpy as np
40
37
41
- import axelrod as axl
42
- from axelrod import Actions , flip_action
43
- from axelrod .strategies .ann import ANN , split_weights
44
- from axelrod_utils import Outputer , score_for , prepare_objective
38
+ from axelrod import Actions
39
+ from axelrod .strategies .ann import ANN
40
+ from evolve_utils import Params , Population , prepare_objective
45
41
46
42
C , D = Actions .C , Actions .D
47
43
48
- ## Neural network specifics
44
+
45
+ ## Todo: mutation decay
46
+ # if decay:
47
+ # mutation_rate *= 0.995
48
+ # mutation_distance *= 0.995
49
+
49
50
50
51
def num_weights (num_features , num_hidden ):
51
52
size = num_features * num_hidden + 2 * num_hidden
52
53
return size
53
54
54
- def random_params (num_features , num_hidden ):
55
- size = num_weights (num_features , num_hidden )
56
- return [random .uniform (- 1 , 1 ) for _ in range (size )]
57
-
58
-
59
- def represent_params (weights ):
60
- """Return a string representing the values of a lookup table dict"""
61
- return ',' .join (map (str , weights ))
62
-
63
-
64
- def params_from_representation (string_id ):
65
- """Return a lookup table dict from a string representing the values"""
66
- return list (map (float , string_id .split (',' )))
67
-
68
55
69
- def copy_params (weights ):
70
- return list (weights )
71
-
72
-
73
- def score_single (weights , objective , num_features , num_hidden ):
74
- args = [weights , num_features , num_hidden ]
75
- return (score_for (ANN , args = args , objective = objective ), weights )
76
-
77
- def score_all (population , pool , objective , num_features , num_hidden ):
78
- results = pool .starmap (
79
- score_single ,
80
- zip (
81
- population ,
82
- repeat (objective ),
83
- repeat (num_features ),
84
- repeat (num_hidden )
56
+ class ANNParams (Params ):
57
+
58
+ def __init__ (self , num_features , num_hidden , mutation_rate = 0.1 ,
59
+ mutation_distance = 5 , weights = None ):
60
+ self .PlayerClass = ANN
61
+ self .num_features = num_features
62
+ self .num_hidden = num_hidden
63
+
64
+ self .mutation_distance = mutation_distance
65
+ if mutation_rate is None :
66
+ size = num_weights (num_features , num_hidden )
67
+ self .mutation_rate = 10 / size
68
+ else :
69
+ self .mutation_rate = mutation_rate
70
+
71
+ if weights is None :
72
+ self .randomize ()
73
+ else :
74
+ # Make sure to copy the lists
75
+ self .weights = list (weights )
76
+
77
+ def player (self ):
78
+ player = self .PlayerClass (self .weights , self .num_features ,
79
+ self .num_hidden )
80
+ return player
81
+
82
+ def copy (self ):
83
+ return ANNParams (
84
+ self .num_features , self .num_hidden , self .mutation_rate ,
85
+ self .mutation_distance , list (self .weights ))
86
+
87
+ def randomize (self ):
88
+ size = num_weights (self .num_features , self .num_hidden )
89
+ self .weights = [random .uniform (- 1 , 1 ) for _ in range (size )]
90
+
91
+ @staticmethod
92
+ def mutate_weights (weights , num_features , num_hidden , mutation_rate ,
93
+ mutation_distance ):
94
+ size = num_weights (num_features , num_hidden )
95
+ randoms = np .random .random (size )
96
+ for i , r in enumerate (randoms ):
97
+ if r < mutation_rate :
98
+ p = 1 + random .uniform (- 1 , 1 ) * mutation_distance
99
+ weights [i ] = weights [i ] * p
100
+ return weights
101
+
102
+ def mutate (self ):
103
+ self .weights = self .mutate_weights (
104
+ self .weights , self .num_features , self .num_hidden ,
105
+ self .mutation_rate , self .mutation_distance )
106
+ # Add in layer sizes?
107
+
108
+ @staticmethod
109
+ def crossover_weights (w1 , w2 ):
110
+ crosspoint = random .randrange (len (w1 ))
111
+ new_weights = list (w1 [:crosspoint ]) + list (w2 [crosspoint :])
112
+ return new_weights
113
+
114
+ def crossover (self , other ):
115
+ # Assuming that the number of states is the same
116
+ new_weights = self .crossover_weights (self .weights , other .weights )
117
+ return ANNParams (
118
+ self .num_features , self .num_hidden , self .mutation_rate ,
119
+ self .mutation_distance , new_weights )
120
+
121
+ def __repr__ (self ):
122
+ return "{}:{}:{}" .format (
123
+ self .num_features ,
124
+ self .num_hidden ,
125
+ ':' .join (map (str , self .weights ))
85
126
)
86
- )
87
- return list (results )
88
-
89
- ## Evolutionary Algorithm
90
-
91
- def crossover (weights_collection ):
92
- copies = []
93
- for i , w1 in enumerate (weights_collection ):
94
- for j , w2 in enumerate (weights_collection ):
95
- if i == j :
96
- continue
97
- crosspoint = random .randrange (len (w1 ))
98
- new_weights = copy_params (w1 [0 :crosspoint ]) + copy_params (w2 [crosspoint :])
99
- copies .append (new_weights )
100
- return copies
101
-
102
- def mutate (copies , mutation_rate , mutation_distance ):
103
- randoms = np .random .random ((len (copies ), 190 ))
104
- for i , c in enumerate (copies ):
105
- for j in range (len (c )):
106
- if randoms [i ][j ] < mutation_rate :
107
- r = 1 + random .uniform (- 1 , 1 ) * mutation_distance
108
- c [j ] = c [j ] * r
109
- return copies
110
-
111
-
112
- def evolve (starting_population , objective , generations , bottleneck ,
113
- mutation_rate , processes , output_filename , param_args ,
114
- mutation_distance ):
115
- """
116
- The function that does everything. Take a set of starting tables, and in
117
- each generation:
118
- - add a bunch more random tables
119
- - simulate recombination between each pair of tables
120
- - randomly mutate the current population of tables
121
- - calculate the fitness function i.e. the average score per turn
122
- - keep the best individuals and discard the rest
123
- - write out summary statistics to the output file
124
- """
125
- pool = Pool (processes = processes )
126
- outputer = Outputer (output_filename )
127
-
128
- current_bests = [[0 , t ] for t in starting_population ]
129
-
130
- for generation in range (generations ):
131
- # Because this is a long-running process we'll just keep appending to
132
- # the output file so we can monitor it while it's running
133
- print ("Starting Generation " + str (generation ))
134
-
135
- # The tables at the start of this generation are the best ones from
136
- # the previous generation (i.e. the second element of each tuple)
137
- # plus a bunch of random ones
138
- random_tables = [random_params (* param_args ) for _ in range (4 )]
139
- tables_to_copy = [copy_params (x [1 ]) for x in current_bests ]
140
- tables_to_copy += random_tables
141
-
142
- # Crossover
143
- copies = crossover (tables_to_copy )
144
- # Mutate
145
- copies = mutate (copies , mutation_rate , mutation_distance )
146
-
147
- # The population of tables we want to consider includes the
148
- # recombined, mutated copies, plus the originals
149
- population = copies + [copy_params (x [1 ]) for x in current_bests ]
150
- # Map the population to get a list of (score, table) tuples
151
-
152
- # This list will be sorted by score, best tables first
153
- results = score_all (population , pool , objective , * param_args )
154
-
155
- # The best tables from this generation become the starting tables
156
- # for the next generation
157
- results .sort (key = itemgetter (0 ), reverse = True )
158
- current_bests = results [0 : bottleneck ]
159
-
160
- # get all the scores for this generation
161
- scores = [score for (score , table ) in results ]
162
-
163
- # Write the data
164
- row = [generation , mean (scores ), pstdev (scores ), results [0 ][0 ],
165
- represent_params (results [0 ][1 ])]
166
- row .extend (results [0 ][1 ])
167
- outputer .write (row )
168
-
169
- print ("Generation" , generation , "| Best Score:" , results [0 ][0 ],
170
- represent_params (results [0 ][1 ]))
171
-
172
- # if decay:
173
- # mutation_rate *= 0.995
174
- # mutation_distance *= 0.995
175
-
176
- return current_bests
177
-
178
127
128
+ @classmethod
129
+ def parse_repr (cls , s ):
130
+ elements = list (map (float , s .split (':' )))
131
+ num_features = elements [0 ]
132
+ num_hidden = elements [1 ]
133
+ weights = elements [2 :]
134
+ return cls (num_features , num_hidden , weights )
179
135
180
136
181
137
if __name__ == '__main__' :
182
- arguments = docopt (__doc__ , version = 'ANN Evolver 0.2 ' )
138
+ arguments = docopt (__doc__ , version = 'ANN Evolver 0.3 ' )
183
139
print (arguments )
184
140
processes = int (arguments ['--processes' ])
185
141
@@ -193,20 +149,17 @@ def evolve(starting_population, objective, generations, bottleneck,
193
149
# Objective
194
150
name = str (arguments ['--objective' ])
195
151
repetitions = int (arguments ['--repetitions' ])
152
+ turns = int (arguments ['--turns' ])
196
153
noise = float (arguments ['--noise' ])
197
154
nmoran = int (arguments ['--nmoran' ])
198
155
199
156
# ANN
200
157
num_features = int (arguments ['--features' ])
201
158
num_hidden = int (arguments ['--hidden' ])
202
159
mutation_distance = float (arguments ['--mu_distance' ])
203
- param_args = [num_features , num_hidden ]
204
-
205
- objective = prepare_objective (name , noise , repetitions , nmoran )
206
-
207
- starting_population = [random_params (* param_args ) for _ in range (population )]
208
-
209
- # strategies = axl.short_run_time_strategies
160
+ param_args = [num_features , num_hidden , mutation_rate , mutation_distance ]
210
161
211
- evolve (starting_population , objective , generations , bottleneck ,
212
- mutation_rate , processes , output_filename , param_args , mutation_distance )
162
+ objective = prepare_objective (name , turns , noise , repetitions , nmoran )
163
+ population = Population (ANNParams , param_args , population , objective ,
164
+ output_filename , bottleneck , processes = processes )
165
+ population .run (generations )
0 commit comments