Skip to content

Commit 9b5d80e

Browse files
Nikoleta-v3drvinceknight
authored andcommitted
Refactor to allow for different algorithms to work on different archetypes (#36)
* create src/dojo/algorithms. Moved te ganetic algorithm class in here. Fix the imports. (some files to my gitignore and some pep8) * create gambler archetype and pso algorithm. The pso_evolve script was modified and the functions have been moved into src/axelrod-dogo/archetype/gambler. The pso algorithm has been implemented as an algorithm and is in src/axelrod-dojo/algorithms/particle_swarm_optimization. Also remove the ability for multiprocessing pso. There is an error that our objective function can not be pickeled. (fixed some pep8) * implement pso algorithm for fsm archetype * add multiprocessing for pso algorithm. Add the check for the pyswarm version as well. I did open an issue but the github account doesn't look active anymore. also fix some pep8 * move scripts to bin. * docstring for the new functions. * made minor corrections. Comments by Marc.
1 parent ebec749 commit 9b5d80e

21 files changed

+588
-183
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,6 @@ target/
5858
data/
5959

6060
*.mypy*
61+
62+
# pycharm
63+
.idea/
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

pso_evolve.py renamed to bin/pso_evolve.py

+7-32
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
88
Usage:
99
pso_evolve.py [-h] [--generations GENERATIONS] [--population POPULATION]
10-
[--processes PROCESSORS] [--output OUTPUT_FILE] [--objective OBJECTIVE]
10+
[--output OUTPUT_FILE] [--objective OBJECTIVE]
1111
[--repetitions REPETITIONS] [--turns TURNS] [--noise NOISE]
1212
[--nmoran NMORAN]
1313
[--plays PLAYS] [--op_plays OP_PLAYS] [--op_start_plays OP_START_PLAYS]
@@ -16,7 +16,6 @@
1616
-h --help Show help
1717
--generations GENERATIONS Generations to run the EA [default: 100]
1818
--population POPULATION Starting population size [default: 40]
19-
--processes PROCESSES Number of processes to use [default: 1]
2019
--output OUTPUT_FILE File to write data to [default: pso_tables.csv]
2120
--objective OBJECTIVE Objective function [default: score]
2221
--repetitions REPETITIONS Repetitions in objective [default: 100]
@@ -29,28 +28,16 @@
2928
"""
3029

3130
from docopt import docopt
32-
import pyswarm
3331

34-
from axelrod import Gambler
35-
from axelrod.strategies.lookerup import (
36-
create_lookup_table_keys, Plays)
37-
from axelrod_dojo import prepare_objective, score_for
32+
from axelrod_dojo.archetypes.gambler import GamblerParams
33+
from axelrod_dojo.algorithms.particle_swarm_optimization import PSO
3834

3935

40-
def optimizepso(param_args, objective, opponents=None):
41-
def f(pattern):
42-
self_plays, op_plays, op_openings = param_args
43-
params = Plays(self_plays=self_plays, op_plays=op_plays,
44-
op_openings=op_openings)
45-
return -score_for(Gambler, objective,
46-
args=[None, None, pattern, params],
47-
opponents=opponents)
48-
return f
36+
from axelrod_dojo.utils import prepare_objective
4937

5038
if __name__ == "__main__":
5139
arguments = docopt(__doc__, version='PSO Evolve 0.3')
5240
print(arguments)
53-
processes = int(arguments['--processes'])
5441

5542
# Population Args
5643
population = int(arguments['--population'])
@@ -71,22 +58,10 @@ def f(pattern):
7158

7259
objective = prepare_objective(name, turns, noise, repetitions, nmoran)
7360

74-
size = len(create_lookup_table_keys(plays, op_plays, op_start_plays))
75-
lb = [0] * size
76-
ub = [1] * size
61+
pso = PSO(GamblerParams, param_args, objective=objective,
62+
population=population, generations=generations)
7763

78-
optimizer = optimizepso(param_args, objective)
64+
xopt, fopt = pso.swarm()
7965

80-
# There is a multiprocessing version (0.7) of pyswarm available at
81-
# https://github.com/tisimst/pyswarm, just pass processes=X
82-
# Pip installs version 0.6
83-
if pyswarm.__version__ == "0.7":
84-
xopt, fopt = pyswarm.pso(
85-
optimizer, lb, ub, swarmsize=population, maxiter=generations,
86-
debug=True, phip=0.8, phig=0.8, omega=0.8, processes=processes)
87-
else:
88-
xopt, fopt = pyswarm.pso(
89-
optimizer, lb, ub, swarmsize=population, maxiter=generations,
90-
debug=True, phip=0.8, phig=0.8, omega=0.8)
9166
print(xopt)
9267
print(fopt)

src/axelrod_dojo/__init__.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from .version import __version__
22
from .archetypes.fsm import FSMParams
3+
from .archetypes.gambler import GamblerParams
4+
from .algorithms.genetic_algorithm import Population
35
from .utils import (prepare_objective,
4-
Population,
56
load_params,
67
Params,
7-
score_for,
88
PlayerInfo)
9+

src/axelrod_dojo/algorithms/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
from itertools import repeat
2+
from multiprocessing import Pool, cpu_count
3+
from operator import itemgetter
4+
from random import randrange
5+
from statistics import mean, pstdev
6+
7+
import axelrod as axl
8+
9+
from axelrod_dojo.utils import Outputer, PlayerInfo, score_params
10+
11+
12+
class Population(object):
13+
"""Population class that implements the evolutionary algorithm."""
14+
def __init__(self, params_class, params_args, size, objective, output_filename,
15+
bottleneck=None, opponents=None, processes=1, weights=None,
16+
sample_count=None, population=None):
17+
self.params_class = params_class
18+
self.bottleneck = bottleneck
19+
if processes == 0:
20+
processes = cpu_count()
21+
self.pool = Pool(processes=processes)
22+
self.outputer = Outputer(output_filename, mode='a')
23+
self.size = size
24+
self.objective = objective
25+
if not bottleneck:
26+
self.bottleneck = size // 4
27+
else:
28+
self.bottleneck = bottleneck
29+
if opponents is None:
30+
self.opponents_information = [
31+
PlayerInfo(s, {}) for s in axl.short_run_time_strategies]
32+
else:
33+
self.opponents_information = [
34+
PlayerInfo(p.__class__, p.init_kwargs) for p in opponents]
35+
self.generation = 0
36+
self.params_args = params_args
37+
38+
if population is not None:
39+
self.population = population
40+
else:
41+
self.population = [params_class(*params_args) for _ in range(self.size)]
42+
43+
self.weights = weights
44+
self.sample_count = sample_count
45+
46+
def score_all(self):
47+
starmap_params = zip(
48+
self.population,
49+
repeat(self.objective),
50+
repeat(self.opponents_information),
51+
repeat(self.weights),
52+
repeat(self.sample_count))
53+
results = self.pool.starmap(score_params, starmap_params)
54+
return results
55+
56+
def subset_population(self, indices):
57+
population = []
58+
for i in indices:
59+
population.append(self.population[i])
60+
self.population = population
61+
62+
@staticmethod
63+
def crossover(population, num_variants):
64+
new_variants = []
65+
for _ in range(num_variants):
66+
i = randrange(len(population))
67+
j = randrange(len(population))
68+
new_variant = population[i].crossover(population[j])
69+
new_variants.append(new_variant)
70+
return new_variants
71+
72+
def evolve(self):
73+
self.generation += 1
74+
print("Scoring Generation {}".format(self.generation))
75+
76+
# Score population
77+
scores = self.score_all()
78+
results = list(zip(scores, range(len(scores))))
79+
results.sort(key=itemgetter(0), reverse=True)
80+
81+
# Report
82+
print("Generation", self.generation, "| Best Score:", results[0][0],
83+
repr(self.population[results[0][1]]))
84+
# Write the data
85+
row = [self.generation, mean(scores), pstdev(scores), results[0][0],
86+
repr(self.population[results[0][1]])]
87+
self.outputer.write(row)
88+
89+
## Next Population
90+
indices_to_keep = [p for (s, p) in results[0: self.bottleneck]]
91+
self.subset_population(indices_to_keep)
92+
# Add mutants of the best players
93+
best_mutants = [p.copy() for p in self.population]
94+
for p in best_mutants:
95+
p.mutate()
96+
self.population.append(p)
97+
# Add random variants
98+
random_params = [self.params_class(*self.params_args)
99+
for _ in range(self.bottleneck // 2)]
100+
params_to_modify = [params.copy() for params in self.population]
101+
params_to_modify += random_params
102+
# Crossover
103+
size_left = self.size - len(params_to_modify)
104+
params_to_modify = self.crossover(params_to_modify, size_left)
105+
# Mutate
106+
for p in params_to_modify:
107+
p.mutate()
108+
self.population += params_to_modify
109+
110+
def __iter__(self):
111+
return self
112+
113+
def __next__(self):
114+
self.evolve()
115+
116+
def run(self, generations):
117+
for _ in range(generations):
118+
next(self)
119+
self.outputer.close()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import axelrod as axl
2+
import pyswarm
3+
4+
from axelrod_dojo.utils import score_params
5+
from axelrod_dojo.utils import PlayerInfo
6+
from multiprocessing import cpu_count
7+
8+
9+
class PSO(object):
10+
"""PSO class that implements a particle swarm optimization algorithm."""
11+
def __init__(self, params_class, params_args, objective, opponents=None,
12+
population=1, generations=1, debug=True, phip=0.8, phig=0.8,
13+
omega=0.8, weights=None, sample_count=None, processes=1):
14+
15+
self.params_class = params_class
16+
self.params_args = params_args
17+
self.objective = objective
18+
if opponents is None:
19+
self.opponents_information = [
20+
PlayerInfo(s, {}) for s in axl.short_run_time_strategies]
21+
else:
22+
self.opponents_information = [
23+
PlayerInfo(p.__class__, p.init_kwargs) for p in opponents]
24+
self.population = population
25+
self.generations = generations
26+
self.debug = debug
27+
self.phip = phip
28+
self.phig = phig
29+
self.omega = omega
30+
self.weights = weights
31+
self.sample_count = sample_count
32+
if processes == 0:
33+
self.processes = cpu_count()
34+
else:
35+
self.processes = processes
36+
37+
def swarm(self):
38+
39+
params = self.params_class(*self.params_args)
40+
lb, ub = params.create_vector_bounds()
41+
42+
def objective_function(vector):
43+
params.receive_vector(vector=vector)
44+
instance_generation_function = 'vector_to_instance'
45+
46+
return - score_params(params=params, objective=self.objective,
47+
opponents_information=self.opponents_information,
48+
weights=self.weights,
49+
sample_count=self.sample_count,
50+
instance_generation_function=instance_generation_function
51+
)
52+
53+
# TODO remove check once v 0.7 is pip installable
54+
# There is a multiprocessing version (0.7) of pyswarm available at
55+
# https://github.com/tisimst/pyswarm, just pass processes=X
56+
# Pip installs version 0.6
57+
if pyswarm.__version__ == "0.7":
58+
xopt, fopt = pyswarm.pso(objective_function, lb, ub,
59+
swarmsize=self.population,
60+
maxiter=self.generations, debug=self.debug,
61+
phip=self.phip, phig=self.phig,
62+
omega=self.omega, processes=self.processes)
63+
else:
64+
xopt, fopt = pyswarm.pso(objective_function, lb, ub,
65+
swarmsize=self.population,
66+
maxiter=self.generations, debug=self.debug,
67+
phip=self.phip, phig=self.phig,
68+
omega=self.omega)
69+
return xopt, fopt

src/axelrod_dojo/archetypes/fsm.py

+41-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import random
2+
import itertools
23
from random import randrange, choice
34

45
import numpy as np
56
from axelrod import Action, FSMPlayer
67
from axelrod.action import UnknownActionError
78

8-
from axelrod_dojo.utils import Params, Population, prepare_objective
9+
from axelrod_dojo.utils import Params
910

1011
C, D = Action.C, Action.D
1112

@@ -33,7 +34,6 @@ def __init__(self, num_states, mutation_rate=None, rows=None,
3334
self.initial_state = initial_state
3435
self.initial_action = initial_action
3536

36-
3737
def player(self):
3838
player = self.PlayerClass(self.rows, self.initial_state,
3939
self.initial_action)
@@ -137,3 +137,42 @@ def parse_repr(cls, s):
137137
rows.append(row)
138138
num_states = len(rows) // 2
139139
return cls(num_states, 0.1, rows, initial_state, initial_action)
140+
141+
def receive_vector(self, vector):
142+
"""Receives a vector and creates an instance attribute called
143+
vector."""
144+
self.vector = vector
145+
146+
def vector_to_instance(self):
147+
"""Turns the attribute vector in to a FSM player instance.
148+
149+
The vector has three parts. The first is used to define the next state
150+
(for each of the player's states - for each opponents action).
151+
152+
The second part is the player's next moves (for each state - for
153+
each opponent's actions).
154+
155+
Finally, a probability to determine the player's first move."""
156+
157+
num_states = int((len(self.vector) - 1) / 4)
158+
state_scale = self.vector[:num_states * 2]
159+
next_states = [int(s * (num_states - 1)) for s in state_scale]
160+
actions = self.vector[num_states * 2: -1]
161+
starting_move = C if round(self.vector[-1]) == 0 else D
162+
163+
fsm = []
164+
for i, (initial_state, action) in enumerate(
165+
itertools.product(range(num_states), [C, D])):
166+
next_action = C if round(actions[i]) == 0 else D
167+
fsm.append([initial_state, action, next_states[i], next_action])
168+
169+
return FSMPlayer(fsm, initial_action=starting_move)
170+
171+
def create_vector_bounds(self):
172+
"""Creates the bounds for the decision variables."""
173+
size = len(self.rows) * 2 + 1
174+
175+
lb = [0] * size
176+
ub = [1] * size
177+
178+
return lb, ub

0 commit comments

Comments
 (0)