Skip to content

Commit

Permalink
Adding cycler (#45)
Browse files Browse the repository at this point in the history
* Added initial Cycler & cycler tests

* changing parameters and adding integration test

* fixed the windows multi threading issue in an ugly way

to fix details see todo comment

* added integration test functionality

* Added docstrings

* Added docstrings, and extra content for custom initial populations

* edited the bug for increasing pop sizes

* Added integration test for pop sizes & seeding players utility

* removed seeding player & windows checks.

* removed numpy from the requirements

* removed global population size var

* adding multi threading fix

* Outputter class changed + tests

* Crossover & tests changed; added header to CSV printing

* Crossover & tests changed; add header to CSV printing; added travis -v

* removed GA output header

* cleaned imports for GA

* made changes to comments & crossover & mutation as requested in pr

* Made changes requested in the PR

* Removed Test for removed functionality

* updated test logging

 - Moved print_output to run method
 - updated docs
 - removed logging in tests to make CI readable
  • Loading branch information
GitToby authored and marcharper committed Mar 10, 2018
1 parent 168da11 commit d1d06b4
Show file tree
Hide file tree
Showing 14 changed files with 399 additions and 58 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ install:
- pip install coveralls
script:
- python setup.py develop
- coverage run --source=src -m unittest discover tests
- coverage run --source=src -m unittest discover tests -v
- coverage report -m
- python doctests.py
after_success:
Expand Down
17 changes: 17 additions & 0 deletions docs/background/genetic_algorithm.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,20 @@ The crossover and mutation are implemented in the following way:
being changed to another random state of
:math:`\delta \times 10^{-1} \times N` (where :math:`N` is the number of
states).

Cycler Sequence Calculator
--------------------------

A Cycler Sequence is the sequence of C & D actions that are passed to the cycler player to follow when playing their
tournament games.

the sequence is found using genetic feature selection:

- Crossover: By working with another cycler player, we take sections of each player and create a new cycler sequence
from the following formula:
let our two player being crossed be called p1 and p2 respectively. we then find the midpoint of both the sequences
and take the first half from p1 and the second half from p2 to combine into the new cycler sequence.

- Mutation: we use a predictor :math:`\delta`to determine if we are going to mutate a
single element in the current sequence. The element, or gene, we change in the sequence is uniformly selected using
the random :code:`package`.
3 changes: 2 additions & 1 deletion docs/tutorial/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ We can now evolve our population::

The :code:`run` command prints out the progress of the algorithm and this is
also written to the output file (we passed :code:`output_filename` as an
argument earlier).
argument earlier). The printing can be turned off to keep logging to a minimum
by passing :code:`print_output=False` to the :code:`run`.

The last best score is a finite state machine with representation
:code:`0:C:0_C_0_C:0_D_1_D:1_C_1_D:1_D_1_D` which corresponds to a strategy that
Expand Down
1 change: 1 addition & 0 deletions src/axelrod_dojo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .archetypes.fsm import FSMParams
from .archetypes.hmm import HMMParams
from .archetypes.gambler import GamblerParams
from .archetypes.cycler import CyclerParams
from .algorithms.genetic_algorithm import Population
from .algorithms.particle_swarm_optimization import PSO
from .utils import (prepare_objective,
Expand Down
48 changes: 31 additions & 17 deletions src/axelrod_dojo/algorithms/genetic_algorithm.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
from itertools import repeat
from itertools import repeat, starmap
from multiprocessing import Pool, cpu_count
from operator import itemgetter
from random import randrange
from statistics import mean, pstdev

import axelrod as axl

from axelrod_dojo.utils import Outputer, PlayerInfo, score_params


class Population(object):
"""Population class that implements the evolutionary algorithm."""

def __init__(self, params_class, params_kwargs, size, objective, output_filename,
bottleneck=None, mutation_probability=.1, opponents=None,
processes=1, weights=None,
sample_count=None, population=None):
self.print_output = True
self.params_class = params_class
self.bottleneck = bottleneck

if processes == 0:
processes = cpu_count()
self.pool = Pool(processes=processes)
self.processes = cpu_count()
else:
self.processes = processes

self.pool = Pool(processes=self.processes)

self.outputer = Outputer(output_filename, mode='a')
self.size = size
self.objective = objective
Expand All @@ -30,10 +34,10 @@ def __init__(self, params_class, params_kwargs, size, objective, output_filename
self.bottleneck = bottleneck
if opponents is None:
self.opponents_information = [
PlayerInfo(s, {}) for s in axl.short_run_time_strategies]
PlayerInfo(s, {}) for s in axl.short_run_time_strategies]
else:
self.opponents_information = [
PlayerInfo(p.__class__, p.init_kwargs) for p in opponents]
PlayerInfo(p.__class__, p.init_kwargs) for p in opponents]
self.generation = 0

self.params_kwargs = params_kwargs
Expand All @@ -50,13 +54,16 @@ def __init__(self, params_class, params_kwargs, size, objective, output_filename
self.sample_count = sample_count

def score_all(self):
starmap_params = zip(
starmap_params_zip = zip(
self.population,
repeat(self.objective),
repeat(self.opponents_information),
repeat(self.weights),
repeat(self.sample_count))
results = self.pool.starmap(score_params, starmap_params)
if self.processes == 1:
results = list(starmap(score_params, starmap_params_zip))
else:
results = self.pool.starmap(score_params, starmap_params_zip)
return results

def subset_population(self, indices):
Expand All @@ -77,22 +84,27 @@ def crossover(population, num_variants):

def evolve(self):
self.generation += 1
print("Scoring Generation {}".format(self.generation))
if self.print_output:
print("Scoring Generation {}".format(self.generation))

# Score population
scores = self.score_all()
results = list(zip(scores, range(len(scores))))
results.sort(key=itemgetter(0), reverse=True)

# Report
print("Generation", self.generation, "| Best Score:", results[0][0],
repr(self.population[results[0][1]]))
if self.print_output:
print("Generation", self.generation, "| Best Score:", results[0][0], repr(self.population[results[0][
1]])) # prints best result
# Write the data
# Note: if using this for analysis, for reproducability it may be useful to
# pass type(opponent) for each of the opponents. This will allow verification of results post run

row = [self.generation, mean(scores), pstdev(scores), results[0][0],
repr(self.population[results[0][1]])]
self.outputer.write(row)
self.outputer.write_row(row)

## Next Population
# Next Population
indices_to_keep = [p for (s, p) in results[0: self.bottleneck]]
self.subset_population(indices_to_keep)
# Add mutants of the best players
Expand All @@ -106,7 +118,7 @@ def evolve(self):
params_to_modify = [params.copy() for params in self.population]
params_to_modify += random_params
# Crossover
size_left = self.size - len(params_to_modify)
size_left = self.size - len(self.population)
params_to_modify = self.crossover(params_to_modify, size_left)
# Mutate
for p in params_to_modify:
Expand All @@ -119,7 +131,9 @@ def __iter__(self):
def __next__(self):
self.evolve()

def run(self, generations):
def run(self, generations, print_output=True):
self.print_output = print_output

for _ in range(generations):
next(self)
self.outputer.close()

119 changes: 119 additions & 0 deletions src/axelrod_dojo/archetypes/cycler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import axelrod as axl
from numpy import random

from axelrod_dojo.utils import Params

C, D = axl.Action


class CyclerParams(Params):
"""
Cycler params is a class to aid with the processes of calculating the best sequence of moves for any given set of
opponents. Each of the population in our algorithm will be an instance of this class for putting into our
genetic lifecycle.
"""

def __init__(self, sequence=None, sequence_length: int = 200, mutation_probability=0.1, mutation_potency=1):
if sequence is None:
# generates random sequence if none is given
self.sequence = self.generate_random_sequence(sequence_length)
else:
# when passing a sequence, make a copy of the sequence to ensure mutation is for the instance only.
self.sequence = list(sequence)

self.sequence_length = len(self.sequence)
self.mutation_probability = mutation_probability
self.mutation_potency = mutation_potency

def __repr__(self):
return "{}".format(self.sequence)

@staticmethod
def generate_random_sequence(sequence_length):
"""
Generate a sequence of random moves
Parameters
----------
sequence_length - length of random moves to generate
Returns
-------
list - a list of C & D actions: list[Action]
"""
# D = axl.Action(0) | C = axl.Action(1)
return list(map(axl.Action, random.randint(0, 2, (sequence_length, 1))))

def crossover(self, other_cycler, in_seed=0):
"""
creates and returns a new CyclerParams instance with a single crossover point in the middle
Parameters
----------
other_cycler - the other cycler where we get the other half of the sequence
Returns
-------
CyclerParams
"""
seq1 = self.sequence
seq2 = other_cycler.sequence

if not in_seed == 0:
# only seed for when we explicitly give it a seed
random.seed(in_seed)

midpoint = int(random.randint(0, len(seq1)) / 2)
new_seq = seq1[:midpoint] + seq2[midpoint:]
return CyclerParams(sequence=new_seq)

def mutate(self):
"""
Basic mutation which may change any random gene(s) in the sequence.
"""
if random.rand() <= self.mutation_probability:
mutated_sequence = self.sequence
for _ in range(self.mutation_potency):
index_to_change = random.randint(0, len(mutated_sequence))
mutated_sequence[index_to_change] = mutated_sequence[index_to_change].flip()
self.sequence = mutated_sequence

def player(self):
"""
Create and return a Cycler player with the sequence that has been generated with this run.
Returns
-------
Cycler(sequence)
"""
return axl.Cycler(self.get_sequence_str())

def copy(self):
"""
Returns a copy of the current cyclerParams
Returns
-------
CyclerParams - a separate instance copy of itself.
"""
# seq length will be provided when copying, no need to pass
return CyclerParams(sequence=self.sequence, mutation_probability=self.mutation_probability)

def get_sequence_str(self):
"""
Concatenate all the actions as a string for constructing Cycler players
[C,D,D,C,D,C] -> "CDDCDC"
[C,C,D,C,C,C] -> "CCDCCC"
[D,D,D,D,D,D] -> "DDDDDD"
Returns
-------
str
"""
string_sequence = ""
for action in self.sequence:
string_sequence += str(action)

return string_sequence
22 changes: 10 additions & 12 deletions src/axelrod_dojo/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from functools import partial
from statistics import mean
import csv
import os

import numpy as np
import axelrod as axl
Expand All @@ -11,17 +10,14 @@
## Output Evolutionary Algorithm results

class Outputer(object):
def __init__(self, filename, mode='w'):
self.output = open(filename, mode)
self.writer = csv.writer(self.output)
def __init__(self, filename, mode='a'):
self.file = filename
self.mode = mode

def write(self, row):
self.writer.writerow(row)
self.output.flush()
os.fsync(self.output.fileno())

def close(self):
self.output.close()
def write_row(self, row):
with open(self.file, self.mode, newline='') as file_writer:
writer = csv.writer(file_writer)
writer.writerow(row)


## Objective functions for optimization
Expand Down Expand Up @@ -105,11 +101,13 @@ def objective_moran_win(me, other, turns, noise, repetitions, N=5,
scores_for_this_opponent.append(0)
return scores_for_this_opponent


# Evolutionary Algorithm


class Params(object):
"""Abstract Base Class for Parameters Objects."""

def mutate(self):
pass

Expand Down Expand Up @@ -147,6 +145,7 @@ def create_vector_bounds(self):
"""Creates the bounds for the decision variables."""
pass


PlayerInfo = namedtuple('PlayerInfo', ['strategy', 'init_kwargs'])


Expand Down Expand Up @@ -193,4 +192,3 @@ def load_params(params_class, filename, num):
for score, rep in all_params[:num]:
best_params.append(parser(rep))
return best_params

Loading

0 comments on commit d1d06b4

Please sign in to comment.