Skip to content

Commit 448ee9e

Browse files
committed
Refactor of evolution algorithm and player class parameters
1 parent 0b0f05a commit 448ee9e

File tree

6 files changed

+757
-595
lines changed

6 files changed

+757
-595
lines changed

ann_evolve.py

Lines changed: 95 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
ann_evolve.py [-h] [--generations GENERATIONS] [--population POPULATION]
1010
[--mu MUTATION_RATE] [--bottleneck BOTTLENECK] [--processes PROCESSORS]
1111
[--output OUTPUT_FILE] [--objective OBJECTIVE] [--repetitions REPETITIONS]
12-
[--noise NOISE] [--nmoran NMORAN]
12+
[--turns TURNS] [--noise NOISE] [--nmoran NMORAN]
1313
[--features FEATURES] [--hidden HIDDEN] [--mu_distance DISTANCE]
1414
1515
Options:
@@ -22,164 +22,120 @@
2222
--output OUTPUT_FILE File to write data to [default: ann_weights.csv]
2323
--objective OBJECTIVE Objective function [default: score]
2424
--repetitions REPETITIONS Repetitions in objective [default: 100]
25+
--turns TURNS Turns in each match [default: 200]
2526
--noise NOISE Match noise [default: 0.00]
2627
--nmoran NMORAN Moran Population Size, if Moran objective [default: 4]
2728
--features FEATURES Number of ANN features [default: 17]
2829
--hidden HIDDEN Number of hidden nodes [default: 10]
2930
--mu_distance DISTANCE Delta max for weights updates [default: 5]
3031
"""
3132

32-
from itertools import repeat
33-
from multiprocessing import Pool
34-
from operator import itemgetter
3533
import random
36-
from statistics import mean, pstdev
3734

3835
from docopt import docopt
3936
import numpy as np
4037

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
4541

4642
C, D = Actions.C, Actions.D
4743

48-
## Neural network specifics
44+
45+
## Todo: mutation decay
46+
# if decay:
47+
# mutation_rate *= 0.995
48+
# mutation_distance *= 0.995
49+
4950

5051
def num_weights(num_features, num_hidden):
5152
size = num_features * num_hidden + 2 * num_hidden
5253
return size
5354

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-
6855

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))
85126
)
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-
178127

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)
179135

180136

181137
if __name__ == '__main__':
182-
arguments = docopt(__doc__, version='ANN Evolver 0.2')
138+
arguments = docopt(__doc__, version='ANN Evolver 0.3')
183139
print(arguments)
184140
processes = int(arguments['--processes'])
185141

@@ -193,20 +149,17 @@ def evolve(starting_population, objective, generations, bottleneck,
193149
# Objective
194150
name = str(arguments['--objective'])
195151
repetitions = int(arguments['--repetitions'])
152+
turns = int(arguments['--turns'])
196153
noise = float(arguments['--noise'])
197154
nmoran = int(arguments['--nmoran'])
198155

199156
# ANN
200157
num_features = int(arguments['--features'])
201158
num_hidden = int(arguments['--hidden'])
202159
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]
210161

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

Comments
 (0)