Skip to content

Commit 168da11

Browse files
gaffney2010marcharper
authored andcommitted
Implemented HMM Archetype (#44)
* Fixed Swap Nodes in mutate_rows per issue #41 Swapped based on state, row[0], then sorted. As per our conversation in issue #41 thread. * Updated for fixed swap in mutation * Moved HMMParams class and implemented tests. * Implemented PSO for HMM. * Implemented PSO for HMM. * Deleted CSVs from bin. * Changed mutation_rate to mutation_probability in test. * Added tests/docstrings to HMM, and fixed bugs in HMM. * Fixed typo * Changed test strategies from hard-to-pickle strategies to basic strategies. * Made read_vector, create_vector_bounds, and vector_to_instance more instance-specific * Made vector changes to FSM too. * Updated test_hmm for vector_to_instance change. * Updated test_gambler for vector_to_instance change. * Fixed test_vector_to_instance in test_fsm. * Made minor formatting fixes. * Put conversion back in. * Cleaned up docstrings.
1 parent aa3837d commit 168da11

File tree

12 files changed

+978
-222
lines changed

12 files changed

+978
-222
lines changed

bin/hmm_evolve.py

Lines changed: 35 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
Hidden Markov Model Evolver
33
44
Usage:
5-
fsm_evolve.py [-h] [--generations GENERATIONS] [--population POPULATION]
5+
hmm_evolve.py [-h] [--generations GENERATIONS] [--population POPULATION]
66
[--mu MUTATION_RATE] [--bottleneck BOTTLENECK] [--processes PROCESSORS]
77
[--output OUTPUT_FILE] [--objective OBJECTIVE] [--repetitions REPETITIONS]
88
[--turns TURNS] [--noise NOISE] [--nmoran NMORAN]
9-
[--states NUM_STATES]
9+
[--states NUM_STATES] [--algorithm ALGORITHM]
1010
1111
Options:
1212
-h --help Show help
@@ -22,190 +22,14 @@
2222
--noise NOISE Match noise [default: 0.00]
2323
--nmoran NMORAN Moran Population Size, if Moran objective [default: 4]
2424
--states NUM_STATES Number of FSM states [default: 5]
25+
--algorithm ALGORITHM Which algorithm to use (EA for evolutionary algorithm or PS for
26+
particle swarm algorithm) [default: EA]
2527
"""
2628

27-
#####
28-
# This is a potential candidate for PSO optimization, which will require
29-
# combining the matrices.
30-
#####
31-
32-
import random
33-
from random import randrange, choice
34-
3529
from docopt import docopt
36-
import numpy as np
37-
38-
from axelrod import Action
39-
from axelrod.strategies.hmm import HMMPlayer
40-
from axelrod_dojo import Params, Population, prepare_objective
41-
42-
C, D = Action.C, Action.D
43-
44-
45-
def copy_lists(rows):
46-
new_rows = list(map(list, rows))
47-
return new_rows
48-
49-
def random_vector(size):
50-
"""Create a random vector of values in [0, 1] that sums to 1."""
51-
vector = []
52-
s = 1
53-
for _ in range(size - 1):
54-
r = s * random.random()
55-
vector.append(r)
56-
s -= r
57-
vector.append(s)
58-
return vector
59-
60-
def normalize_vector(vec):
61-
s = sum(vec)
62-
vec = [v / s for v in vec]
63-
return vec
64-
65-
def mutate_row(row, mutation_rate):
66-
randoms = np.random.random(len(row))
67-
for i in range(len(row)):
68-
if randoms[i] < mutation_rate:
69-
ep = random.uniform(-1, 1) / 4
70-
row[i] += ep
71-
if row[i] < 0:
72-
row[i] = 0
73-
if row[i] > 1:
74-
row[i] = 1
75-
return row
76-
77-
78-
class HMMParams(Params):
79-
80-
def __init__(self, num_states, mutation_rate=None, transitions_C=None,
81-
transitions_D=None, emission_probabilities=None,
82-
initial_state=0, initial_action=C):
83-
self.PlayerClass = HMMPlayer
84-
self.num_states = num_states
85-
if mutation_rate is None:
86-
self.mutation_rate = 10 / (num_states ** 2)
87-
else:
88-
self.mutation_rate = mutation_rate
89-
if transitions_C is None:
90-
self.randomize()
91-
else:
92-
# Make sure to copy the lists
93-
self.transitions_C = copy_lists(transitions_C)
94-
self.transitions_D = copy_lists(transitions_D)
95-
self.emission_probabilities = list(emission_probabilities)
96-
self.initial_state = initial_state
97-
self.initial_action = initial_action
98-
9930

100-
def player(self):
101-
player = self.PlayerClass(self.transitions_C, self.transitions_D,
102-
self.emission_probabilities,
103-
self.initial_state, self.initial_action)
104-
return player
105-
106-
def copy(self):
107-
return HMMParams(self.num_states, self.mutation_rate,
108-
self.transitions_C, self.transitions_D,
109-
self.emission_probabilities,
110-
self.initial_state, self.initial_action)
111-
112-
@staticmethod
113-
def random_params(num_states):
114-
t_C = []
115-
t_D = []
116-
for _ in range(num_states):
117-
t_C.append(random_vector(num_states))
118-
t_D.append(random_vector(num_states))
119-
initial_state = randrange(num_states)
120-
# initial_action = choice([C, D])
121-
initial_action = C
122-
return t_C, t_D, initial_state, initial_action
123-
124-
def randomize(self):
125-
t_C, t_D, initial_state, initial_action = self.random_params(self.num_states)
126-
self.emission_probabilities = [random.random() for _ in range(self.num_states)]
127-
self.transitions_C = t_C
128-
self.transitions_D = t_D
129-
self.initial_state = initial_state
130-
self.initial_action = initial_action
131-
132-
@staticmethod
133-
def mutate_rows(rows, mutation_rate):
134-
for i, row in enumerate(rows):
135-
row = mutate_row(row, mutation_rate)
136-
rows[i] = normalize_vector(row)
137-
return rows
138-
139-
def mutate(self):
140-
self.transitions_C = self.mutate_rows(
141-
self.transitions_C, self.mutation_rate)
142-
self.transitions_D = self.mutate_rows(
143-
self.transitions_D, self.mutation_rate)
144-
self.emission_probabilities = mutate_row(
145-
self.emission_probabilities, self.mutation_rate)
146-
if random.random() < self.mutation_rate / 10:
147-
self.initial_action = self.initial_action.flip()
148-
if random.random() < self.mutation_rate / (10 * self.num_states):
149-
self.initial_state = randrange(self.num_states)
150-
# Change node size?
151-
152-
@staticmethod
153-
def crossover_rows(rows1, rows2):
154-
num_states = len(rows1)
155-
crosspoint = randrange(num_states)
156-
new_rows = copy_lists(rows1[:crosspoint])
157-
new_rows += copy_lists(rows2[crosspoint:])
158-
return new_rows
159-
160-
@staticmethod
161-
def crossover_weights(w1, w2):
162-
crosspoint = random.randrange(len(w1))
163-
new_weights = list(w1[:crosspoint]) + list(w2[crosspoint:])
164-
return new_weights
165-
166-
def crossover(self, other):
167-
# Assuming that the number of states is the same
168-
t_C = self.crossover_rows(self.transitions_C, other.transitions_C)
169-
t_D = self.crossover_rows(self.transitions_D, other.transitions_D)
170-
emissions = self.crossover_weights(
171-
self.emission_probabilities, other.emission_probabilities)
172-
return HMMParams(self.num_states, self.mutation_rate,
173-
t_C, t_D, emissions,
174-
self.initial_state, self.initial_action)
175-
176-
@staticmethod
177-
def repr_rows(rows):
178-
ss = []
179-
for row in rows:
180-
ss.append("_".join(list(map(str, row))))
181-
return "|".join(ss)
182-
183-
def __repr__(self):
184-
return "{}:{}:{}:{}:{}".format(
185-
self.initial_state,
186-
self.initial_action,
187-
self.repr_rows(self.transitions_C),
188-
self.repr_rows(self.transitions_D),
189-
self.repr_rows([self.emission_probabilities])
190-
)
191-
192-
@classmethod
193-
def parse_repr(cls, s):
194-
def parse_matrix(sm):
195-
rows = []
196-
lines = sm.split('|')
197-
for line in lines:
198-
row = line.split('_')
199-
row = list(map(float, row))
200-
rows.append(row)
201-
return row
202-
lines = s.split(':')
203-
initial_state = int(lines[0])
204-
initial_action = lines[1]
205-
t_C = parse_matrix(lines[2])
206-
t_D = parse_matrix(lines[3])
207-
ps = parse_matrix(lines[4])
208-
return cls(t_C, t_D, ps, initial_state, initial_action)
31+
from axelrod_dojo import HMMParams, Population, prepare_objective
32+
from axelrod_dojo.algorithms.particle_swarm_optimization import PSO
20933

21034

21135
if __name__ == '__main__':
@@ -227,13 +51,33 @@ def parse_matrix(sm):
22751
noise = float(arguments['--noise'])
22852
nmoran = int(arguments['--nmoran'])
22953

230-
# FSM
54+
# HMM
23155
num_states = int(arguments['--states'])
232-
param_kwargs = {"num_states": num_states}
233-
234-
objective = prepare_objective(name, turns, noise, repetitions, nmoran)
235-
population = Population(HMMParams, param_kwargs, population, objective,
236-
output_filename, bottleneck,
237-
mutation_probability,
238-
processes=processes)
239-
population.run(generations)
56+
params_kwargs = {"num_states": num_states}
57+
58+
if arguments['--algorithm'] == "PS":
59+
objective = prepare_objective(name, turns, noise, repetitions, nmoran)
60+
pso = PSO(HMMParams, params_kwargs, objective=objective,
61+
population=population, generations=generations,
62+
size=num_states)
63+
64+
xopt_helper, fopt = pso.swarm()
65+
xopt = HMMParams(num_states=num_states)
66+
xopt.read_vector(xopt_helper, num_states)
67+
else:
68+
objective = prepare_objective(name, turns, noise, repetitions, nmoran)
69+
population = Population(HMMParams, params_kwargs, population, objective,
70+
output_filename, bottleneck, mutation_probability,
71+
processes=processes)
72+
population.run(generations)
73+
74+
# Get the best member of the population to output.
75+
scores = population.score_all()
76+
record, record_holder = 0, -1
77+
for i, s in enumerate(scores):
78+
if s >= record:
79+
record = s
80+
record_holder = i
81+
xopt, fopt = population.population[record_holder], record
82+
83+
print("Best Score: {} {}".format(fopt, xopt))

bin/pso_evolve.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,13 @@
5454
plays = int(arguments['--plays'])
5555
op_plays = int(arguments['--op_plays'])
5656
op_start_plays = int(arguments['--op_start_plays'])
57-
param_kwargs = {"plays": plays,
57+
params_kwargs = {"plays": plays,
5858
"op_plays": op_plays,
5959
"op_state_plays": op_start_plays}
6060

6161
objective = prepare_objective(name, turns, noise, repetitions, nmoran)
6262

63-
pso = PSO(GamblerParams, param_kwargs, objective=objective,
63+
pso = PSO(GamblerParams, params_kwargs, objective=objective,
6464
population=population, generations=generations)
6565

6666
xopt, fopt = pso.swarm()

docs/background/genetic_algorithm.rst

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,44 @@ The crossover and mutation are implemented in the following way:
5151
- Crossover: this is done by taking a randomly selected number of target
5252
state/actions
5353
pairs from one individual and the rest from the other.
54-
- Mutation: given a mutation probability :math:`delta` each target state/action
54+
- Mutation: given a mutation probability :math:`\delta` each target state/action
5555
has a probability :math:`\delta` of being randomly changed to one of the other
5656
states or actions. Furthermore the **initial** action has a probability of
5757
being swapped of :math:`\delta\times 10^{-1}` and the **initial** state has a
5858
probability of being changed to another random state of :math:`\delta \times
5959
10^{-1} \times N` (where :math:`N` is the number of states).
60+
61+
Hidden Markov models
62+
---------------------
63+
64+
A hidden Markov model is made up of the following:
65+
66+
- a mapping from a state/action pair to a probability of defect or cooperation.
67+
- a cooperation transition matrix, the probability of transitioning to each
68+
state, given current state and an opponent cooperation.
69+
- a defection transition matrix, the probability of transitioning to each
70+
state, given current state and an opponent defection.
71+
- an initial state/action pair.
72+
73+
(See [Harper2017]_ for more details.)
74+
75+
The crossover and mutation are implemented in the following way:
76+
77+
- Crossover: this is done by taking a randomly selected number of rows from
78+
one cooperation transition matrix and the rest from the other to form a target
79+
cooperation transition matrix; then a different number of randomly selected
80+
rows from one defection transition matrix and the rest from the other; and
81+
then a randomly select number of entries from one state/part -> probability
82+
mapping and the rest from the other.
83+
- Mutation: given a mutation probability :math:`delta` each cell of both
84+
transition matrices and the state/part -> probability mapping have probability
85+
:math:`delta` of being increased by :math:`varepsilon`, where
86+
:math:`varepsilon` is randomly drawn uniformly from :math:`[-0.25, 0.25]`
87+
(A negative number would decrease.) Then the transition matrices and mapping
88+
are adjusted so that no cell is outside :math:`[0, 1]` and the transition
89+
matrices are normalized so that each row adds to 1. Furthermore the
90+
**initial** action has a probability of being swapped of
91+
:math:`\delta\times 10^{-1}` and the **initial** state has a probability of
92+
being changed to another random state of
93+
:math:`\delta \times 10^{-1} \times N` (where :math:`N` is the number of
94+
states).

src/axelrod_dojo/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from .version import __version__
22
from .archetypes.fsm import FSMParams
3+
from .archetypes.hmm import HMMParams
34
from .archetypes.gambler import GamblerParams
45
from .algorithms.genetic_algorithm import Population
6+
from .algorithms.particle_swarm_optimization import PSO
57
from .utils import (prepare_objective,
68
load_params,
79
Params,

src/axelrod_dojo/algorithms/particle_swarm_optimization.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def swarm(self):
4141

4242
def objective_function(vector):
4343
params.receive_vector(vector=vector)
44-
instance_generation_function = 'vector_to_instance'
44+
instance_generation_function = 'player'
4545

4646
return - score_params(params=params, objective=self.objective,
4747
opponents_information=self.opponents_information,

src/axelrod_dojo/archetypes/fsm.py

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -140,34 +140,30 @@ def parse_repr(cls, s):
140140
return cls(num_states, rows, initial_state, initial_action)
141141

142142
def receive_vector(self, vector):
143-
"""Receives a vector and creates an instance attribute called
144-
vector."""
145-
self.vector = vector
146-
147-
def vector_to_instance(self):
148-
"""Turns the attribute vector in to a FSM player instance.
143+
"""
144+
Read a serialized vector into the set of FSM parameters (less initial
145+
state). Then assign those FSM parameters to this class instance.
149146
150147
The vector has three parts. The first is used to define the next state
151148
(for each of the player's states - for each opponents action).
152149
153150
The second part is the player's next moves (for each state - for
154151
each opponent's actions).
155152
156-
Finally, a probability to determine the player's first move."""
157-
158-
num_states = int((len(self.vector) - 1) / 4)
159-
state_scale = self.vector[:num_states * 2]
160-
next_states = [int(s * (num_states - 1)) for s in state_scale]
161-
actions = self.vector[num_states * 2: -1]
162-
starting_move = C if round(self.vector[-1]) == 0 else D
153+
Finally, a probability to determine the player's first move.
154+
"""
155+
state_scale = vector[:self.num_states * 2]
156+
next_states = [int(s * (self.num_states - 1)) for s in state_scale]
157+
actions = vector[self.num_states * 2: -1]
158+
159+
self.initial_action = C if round(vector[-1]) == 0 else D
160+
self.initial_state = 1
163161

164-
fsm = []
162+
self.rows = []
165163
for i, (initial_state, action) in enumerate(
166-
itertools.product(range(num_states), [C, D])):
164+
itertools.product(range(self.num_states), [C, D])):
167165
next_action = C if round(actions[i]) == 0 else D
168-
fsm.append([initial_state, action, next_states[i], next_action])
169-
170-
return FSMPlayer(fsm, initial_action=starting_move)
166+
self.rows.append([initial_state, action, next_states[i], next_action])
171167

172168
def create_vector_bounds(self):
173169
"""Creates the bounds for the decision variables."""

0 commit comments

Comments
 (0)