Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added some type hints, added requirements.txt, improved gitignore #3

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ continuous
rules
tsp
multi
symbreg
symbreg
.idea
venv
93 changes: 57 additions & 36 deletions partition.py
Original file line number Diff line number Diff line change
@@ -1,71 +1,86 @@
import random
import numpy as np
from typing import TypeVar, List, Callable, Tuple
import functools

import utils

K = 10 #number of piles
POP_SIZE = 100 # population size
MAX_GEN = 500 # maximum number of generations
CX_PROB = 0.8 # crossover probability
MUT_PROB = 0.2 # mutation probability
MUT_FLIP_PROB = 0.1 # probability of chaninging value during mutation
REPEATS = 10 # number of runs of algorithm (should be at least 10)
OUT_DIR = 'partition' # output directory for logs
EXP_ID = 'default' # the ID of this experiment (used to create log names)
K = 10 # number of piles
POP_SIZE = 100 # population size
MAX_GEN = 500 # maximum number of generations
CX_PROB = 0.8 # crossover probability
MUT_PROB = 0.2 # mutation probability
MUT_FLIP_PROB = 0.1 # probability of chaninging value during mutation
REPEATS = 10 # number of runs of algorithm (should be at least 10)
OUT_DIR = 'partition' # output directory for logs
EXP_ID = 'default' # the ID of this experiment (used to create log names)

Individual = TypeVar("Individual")
Population = List[Individual]
Operator = Callable[[Population], Population]
FitnessFunction = Callable[[Individual], utils.FitObjPair]


# reads the input set of values of objects
def read_weights(filename):
def read_weights(filename: str) -> List[int]:
with open(filename) as f:
return list(map(int, f.readlines()))


# computes the bin weights
# - bins are the indices of bins into which the object belongs
def bin_weights(weights, bins):
bw = [0]*K
bw = [0] * K
for w, b in zip(weights, bins):
bw[b] += w
return bw


# the fitness function
def fitness(ind, weights):
def fitness(ind: Individual, weights: List[int]) -> utils.FitObjPair:
bw = bin_weights(weights, ind)
return utils.FitObjPair(fitness=1/(max(bw) - min(bw) + 1),
return utils.FitObjPair(fitness=1 / (max(bw) - min(bw) + 1),
objective=max(bw) - min(bw))


# creates the individual
def create_ind(ind_len):
def create_ind(ind_len: int) -> Individual:
return [random.randrange(0, K) for _ in range(ind_len)]


# creates the population using the create individual function
def create_pop(pop_size, create_individual):
def create_pop(pop_size: int, create_individual: Callable[[], Population]) -> Population:
return [create_individual() for _ in range(pop_size)]


# the roulette wheel selection
def roulette_wheel_selection(pop, fits, k):
def roulette_wheel_selection(pop: Population, fits: List[float], k) -> Population:
return random.choices(pop, fits, k=k)


# implements the one-point crossover of two individuals
def one_pt_cross(p1, p2):
def one_pt_cross(p1: Individual, p2: Individual) -> (Individual, Individual):
point = random.randrange(1, len(p1))
o1 = p1[:point] + p2[point:]
o2 = p2[:point] + p1[point:]
return o1, o2


# implements the "bit-flip" mutation of one individual
def flip_mutate(p, prob, upper):
def flip_mutate(p: Individual, prob: float, upper: int) -> Individual:
return [random.randrange(0, upper) if random.random() < prob else i for i in p]

# applies a list of genetic operators (functions with 1 argument - population)

# applies a list of genetic operators (functions with 1 argument - population)
# to the population
def mate(pop, operators):
def mate(pop: Population, operators: List[Operator]):
for o in operators:
pop = o(pop)
return pop


# applies the cross function (implementing the crossover of two individuals)
# to the whole population (with probability cx_prob)
def crossover(pop, cross, cx_prob):
def crossover(pop: Population, cross: Callable[[Individual, Individual], Tuple[Individual, Individual]],
cx_prob: float) -> Population:
off = []
for p1, p2 in zip(pop[0::2], pop[1::2]):
if random.random() < cx_prob:
Expand All @@ -76,11 +91,13 @@ def crossover(pop, cross, cx_prob):
off.append(o2)
return off


# applies the mutate function (implementing the mutation of a single individual)
# to the whole population with probability mut_prob)
def mutation(pop, mutate, mut_prob):
def mutation(pop: Population, mutate: Callable[[Individual], Individual], mut_prob: float) -> Population:
return [mutate(p) if random.random() < mut_prob else p[:] for p in pop]


# implements the evolutionary algorithm
# arguments:
# pop_size - the initial population
Expand All @@ -95,7 +112,9 @@ def mutation(pop, mutate, mut_prob):
# map_fn - function to use to map fitness evaluation over the whole
# population (default `map`)
# log - a utils.Log structure to log the evolution run
def evolutionary_algorithm(pop, max_gen, fitness, operators, mate_sel, *, map_fn=map, log=None):
def evolutionary_algorithm(pop: Population, max_gen: int, fitness: FitnessFunction,
operators: List[Operator], mate_sel: Callable[[Population, List[float], int], Population], *,
map_fn=map, log=None) -> Population:
evals = 0
for G in range(max_gen):
fits_objs = list(map_fn(fitness, pop))
Expand All @@ -111,20 +130,22 @@ def evolutionary_algorithm(pop, max_gen, fitness, operators, mate_sel, *, map_fn

return pop


if __name__ == '__main__':
# read the weights from input
weights = read_weights('inputs/partition-easy.txt')

# use `functool.partial` to create fix some arguments of the functions
# and create functions with required signatures
cr_ind = functools.partial(create_ind, ind_len=len(weights))
fit = functools.partial(fitness, weights=weights)
xover = functools.partial(crossover, cross=one_pt_cross, cx_prob=CX_PROB)
mut = functools.partial(mutation, mut_prob=MUT_PROB,
mutate=functools.partial(flip_mutate, prob=MUT_FLIP_PROB, upper=K))
cr_ind: Callable[[], Individual] = functools.partial(create_ind, ind_len=len(weights))
fit: FitnessFunction = functools.partial(fitness, weights=weights)
xover: Operator = functools.partial(crossover, cross=one_pt_cross, cx_prob=CX_PROB)
mut: Operator = functools.partial(mutation, mut_prob=MUT_PROB,
mutate=functools.partial(flip_mutate, prob=MUT_FLIP_PROB, upper=K))

# we can use multiprocessing to evaluate fitness in parallel
import multiprocessing

pool = multiprocessing.Pool()

import matplotlib.pyplot as plt
Expand All @@ -134,20 +155,20 @@ def evolutionary_algorithm(pop, max_gen, fitness, operators, mate_sel, *, map_fn
best_inds = []
for run in range(REPEATS):
# initialize the log structure
log = utils.Log(OUT_DIR, EXP_ID, run,
write_immediately=True, print_frequency=5)
log = utils.Log(OUT_DIR, EXP_ID, run, write_immediately=True, print_frequency=5)
# create population
pop = create_pop(POP_SIZE, cr_ind)
# run evolution - notice we use the pool.map as the map_fn
pop = evolutionary_algorithm(pop, MAX_GEN, fit, [xover, mut], roulette_wheel_selection, map_fn=pool.map, log=log)
pop = evolutionary_algorithm(pop, MAX_GEN, fit, [xover, mut], roulette_wheel_selection, map_fn=pool.map,
log=log)
# remember the best individual from last generation, save it to file
bi = max(pop, key=fit)
best_inds.append(bi)

with open(f'{OUT_DIR}/{EXP_ID}_{run}.best', 'w') as f:
for w, b in zip(weights, bi):
f.write(f'{w} {b}\n')

# if we used write_immediately = False, we would need to save the
# files now
# log.write_files()
Expand All @@ -162,7 +183,7 @@ def evolutionary_algorithm(pop, max_gen, fitness, operators, mate_sel, *, map_fn
# read the summary log and plot the experiment
evals, lower, mean, upper = utils.get_plot_data(OUT_DIR, EXP_ID)
plt.figure(figsize=(12, 8))
utils.plot_experiment(evals, lower, mean, upper, legend_name = 'Default settings')
utils.plot_experiment(evals, lower, mean, upper, legend_name='Default settings')
plt.legend()
plt.show()

Expand All @@ -173,4 +194,4 @@ def evolutionary_algorithm(pop, max_gen, fitness, operators, mate_sel, *, map_fn
# rename_dict={'default': 'Default setting'})
# the rename_dict can be used to make reasonable entries in the legend -
# experiments that are not in the dict use their id (in this case, the
# legend entries would be 'Default settings' and 'tuned')
# legend entries would be 'Default settings' and 'tuned')
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
numpy
pandas
matplotlib