Skip to content

Commit

Permalink
Merge pull request DEAP#149 from cyrilpic/choosable_attr
Browse files Browse the repository at this point in the history
Adapt selectors to use choosable attributes as criterion
  • Loading branch information
fmder authored Feb 20, 2017
2 parents a1412d7 + 0501efa commit 98d44fc
Show file tree
Hide file tree
Showing 2 changed files with 217 additions and 19 deletions.
44 changes: 25 additions & 19 deletions deap/tools/selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,36 +24,39 @@ def selRandom(individuals, k):
return [random.choice(individuals) for i in xrange(k)]


def selBest(individuals, k):
def selBest(individuals, k, fit_attr="fitness"):
"""Select the *k* best individuals among the input *individuals*. The
list returned contains references to the input *individuals*.
:param individuals: A list of individuals to select from.
:param k: The number of individuals to select.
:param fit_attr: The attribute of individuals to use as selection criterion
:returns: A list containing the k best individuals.
"""
return sorted(individuals, key=attrgetter("fitness"), reverse=True)[:k]
return sorted(individuals, key=attrgetter(fit_attr), reverse=True)[:k]


def selWorst(individuals, k):
def selWorst(individuals, k, fit_attr="fitness"):
"""Select the *k* worst individuals among the input *individuals*. The
list returned contains references to the input *individuals*.
:param individuals: A list of individuals to select from.
:param k: The number of individuals to select.
:param fit_attr: The attribute of individuals to use as selection criterion
:returns: A list containing the k worst individuals.
"""
return sorted(individuals, key=attrgetter("fitness"))[:k]
return sorted(individuals, key=attrgetter(fit_attr))[:k]


def selTournament(individuals, k, tournsize):
def selTournament(individuals, k, tournsize, fit_attr="fitness"):
"""Select *k* individuals from the input *individuals* using *k*
tournaments of *tournsize* individuals. The list returned contains
references to the input *individuals*.
:param individuals: A list of individuals to select from.
:param k: The number of individuals to select.
:param tournsize: The number of individuals participating in each tournament.
:param fit_attr: The attribute of individuals to use as selection criterion
:returns: A list of selected individuals.
This function uses the :func:`~random.choice` function from the python base
Expand All @@ -62,17 +65,18 @@ def selTournament(individuals, k, tournsize):
chosen = []
for i in xrange(k):
aspirants = selRandom(individuals, tournsize)
chosen.append(max(aspirants, key=attrgetter("fitness")))
chosen.append(max(aspirants, key=attrgetter(fit_attr)))
return chosen

def selRoulette(individuals, k):
def selRoulette(individuals, k, fit_attr="fitness"):
"""Select *k* individuals from the input *individuals* using *k*
spins of a roulette. The selection is made by looking only at the first
objective of each individual. The list returned contains references to
the input *individuals*.
:param individuals: A list of individuals to select from.
:param k: The number of individuals to select.
:param fit_attr: The attribute of individuals to use as selection criterion
:returns: A list of selected individuals.
This function uses the :func:`~random.random` function from the python base
Expand All @@ -82,23 +86,23 @@ def selRoulette(individuals, k):
The roulette selection by definition cannot be used for minimization
or when the fitness can be smaller or equal to 0.
"""
s_inds = sorted(individuals, key=attrgetter("fitness"), reverse=True)
sum_fits = sum(ind.fitness.values[0] for ind in individuals)


s_inds = sorted(individuals, key=attrgetter(fit_attr), reverse=True)
sum_fits = sum(getattr(ind, fit_attr).values[0] for ind in individuals)
chosen = []
for i in xrange(k):
u = random.random() * sum_fits
sum_ = 0
for ind in s_inds:
sum_ += ind.fitness.values[0]
sum_ += getattr(ind, fit_attr).values[0]
if sum_ > u:
chosen.append(ind)
break

return chosen


def selDoubleTournament(individuals, k, fitness_size, parsimony_size, fitness_first):
def selDoubleTournament(individuals, k, fitness_size, parsimony_size, fitness_first, fit_attr="fitness"):
"""Tournament selection which use the size of the individuals in order
to discriminate good solutions. This kind of tournament is obviously
useless with fixed-length representation, but has been shown to
Expand Down Expand Up @@ -133,6 +137,7 @@ def selDoubleTournament(individuals, k, fitness_size, parsimony_size, fitness_fi
(size tournament feeding fitness tournaments with candidates). It has been \
shown that this parameter does not have a significant effect in most cases\
(see [Luke2002fighting]_).
:param fit_attr: The attribute of individuals to use as selection criterion
:returns: A list of selected individuals.
.. [Luke2002fighting] Luke and Panait, 2002, Fighting bloat with
Expand Down Expand Up @@ -164,7 +169,7 @@ def _fitTournament(individuals, k, select):
chosen = []
for i in xrange(k):
aspirants = select(individuals, k=fitness_size)
chosen.append(max(aspirants, key=attrgetter("fitness")))
chosen.append(max(aspirants, key=attrgetter(fit_attr)))
return chosen

if fitness_first:
Expand All @@ -174,21 +179,22 @@ def _fitTournament(individuals, k, select):
tsize = partial(_sizeTournament, select=selRandom)
return _fitTournament(individuals, k, tsize)

def selStochasticUniversalSampling(individuals, k):
def selStochasticUniversalSampling(individuals, k, fit_attr="fitness"):
"""Select the *k* individuals among the input *individuals*.
The selection is made by using a single random value to sample all of the
individuals by choosing them at evenly spaced intervals. The list returned
contains references to the input *individuals*.
:param individuals: A list of individuals to select from.
:param k: The number of individuals to select.
:param fit_attr: The attribute of individuals to use as selection criterion
:return: A list of selected individuals.
This function uses the :func:`~random.uniform` function from the python base
:mod:`random` module.
"""
s_inds = sorted(individuals, key=attrgetter("fitness"), reverse=True)
sum_fits = sum(ind.fitness.values[0] for ind in individuals)
s_inds = sorted(individuals, key=attrgetter(fit_attr), reverse=True)
sum_fits = sum(getattr(ind, fit_attr).values[0] for ind in individuals)

distance = sum_fits / float(k)
start = random.uniform(0, distance)
Expand All @@ -197,10 +203,10 @@ def selStochasticUniversalSampling(individuals, k):
chosen = []
for p in points:
i = 0
sum_ = s_inds[i].fitness.values[0]
sum_ = getattr(s_inds[i], fit_attr).values[0]
while sum_ < p:
i += 1
sum_ += s_inds[i].fitness.values[0]
sum_ += getattr(s_inds[i], fit_attr).values[0]
chosen.append(s_inds[i])

return chosen
Expand Down
192 changes: 192 additions & 0 deletions examples/ga/mo_rhv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# This file is part of DEAP.
#
# DEAP is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
#
# DEAP is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with DEAP. If not, see <http://www.gnu.org/licenses/>.

# Regular Hypervolume-based Algorithm (greedy version)

import array
import random
import json

import numpy

from math import sqrt

from deap import algorithms
from deap import base
from deap import benchmarks
from deap.benchmarks.tools import diversity, convergence, hypervolume
from deap.tools.indicator import hv
from deap import creator
from deap import tools


creator.create("FitnessMin", base.Fitness, weights=(-1.0, -1.0))
# Hypervolume contribution
creator.create("FitnessHV", base.Fitness, weights=(1.0,))
creator.create("Individual", array.array, typecode='d',
fitness=creator.FitnessMin, fitness_hv=creator.FitnessHV)

toolbox = base.Toolbox()

# Problem definition
# Functions zdt1, zdt2, zdt3, zdt6 have bounds [0, 1]
BOUND_LOW, BOUND_UP = 0.0, 1.0

# Functions zdt4 has bounds x1 = [0, 1], xn = [-5, 5], with n = 2, ..., 10
# BOUND_LOW, BOUND_UP = [0.0] + [-5.0]*9, [1.0] + [5.0]*9

# Functions zdt1, zdt2, zdt3 have 30 dimensions, zdt4 and zdt6 have 10
NDIM = 30

def uniform(low, up, size=None):
try:
return [random.uniform(a, b) for a, b in zip(low, up)]
except TypeError:
return [random.uniform(a, b) for a, b in zip([low] * size, [up] * size)]


def hypervolume_contrib(front, **kargs):
"""Returns the hypervolume contribution of each individual. The provided
*front* should be a set of non-dominated individuals having each a
:attr:`fitness` attribute.
"""
# Must use wvalues * -1 since hypervolume use implicit minimization
# And minimization in deap use max on -obj
wobj = numpy.array([ind.fitness.wvalues for ind in front]) * -1
ref = kargs.get("ref", None)
if ref is None:
ref = numpy.max(wobj, axis=0) + 1

total_hv = hv.hypervolume(wobj, ref)

def contribution(i):
# The contribution of point p_i in point set P
# is the hypervolume of P without p_i
return total_hv - hv.hypervolume(numpy.concatenate((wobj[:i], wobj[i+1:])), ref)

# Parallelization note: Cannot pickle local function
return map(contribution, range(len(front)))


toolbox.register("attr_float", uniform, BOUND_LOW, BOUND_UP, NDIM)
toolbox.register("individual", tools.initIterate, creator.Individual, toolbox.attr_float)
toolbox.register("population", tools.initRepeat, list, toolbox.individual)

toolbox.register("evaluate", benchmarks.zdt1)
toolbox.register("mate", tools.cxSimulatedBinaryBounded, low=BOUND_LOW, up=BOUND_UP, eta=20.0)
toolbox.register("mutate", tools.mutPolynomialBounded, low=BOUND_LOW, up=BOUND_UP, eta=20.0, indpb=1.0/NDIM)
toolbox.register("sort", tools.sortLogNondominated)
# Selection is based on HV fitness
toolbox.register("select", tools.selBest, fit_attr="fitness_hv")

def main(seed=None):
random.seed(seed)

NGEN = 250
MU = 100
CXPB = 0.9

stats = tools.Statistics(lambda ind: ind.fitness.values)
# stats.register("avg", numpy.mean, axis=0)
# stats.register("std", numpy.std, axis=0)
stats.register("min", numpy.min, axis=0)
stats.register("max", numpy.max, axis=0)

logbook = tools.Logbook()
logbook.header = "gen", "evals", "std", "min", "avg", "max"

pop = toolbox.population(n=MU)

# Evaluate the individuals with an invalid fitness
invalid_ind = [ind for ind in pop if not ind.fitness.valid]
fitnesses = toolbox.map(toolbox.evaluate, invalid_ind)
for ind, fit in zip(invalid_ind, fitnesses):
ind.fitness.values = fit

record = stats.compile(pop)
logbook.record(gen=0, evals=len(invalid_ind), **record)
print(logbook.stream)

# Begin the generational process
for gen in range(1, NGEN):
# Vary the population
offspring = tools.selRandom(pop, len(pop))
offspring = [toolbox.clone(ind) for ind in offspring]

for ind1, ind2 in zip(offspring[::2], offspring[1::2]):
if random.random() <= CXPB:
toolbox.mate(ind1, ind2)

toolbox.mutate(ind1)
toolbox.mutate(ind2)
del ind1.fitness.values, ind2.fitness.values

# Evaluate the individuals with an invalid fitness
invalid_ind = [ind for ind in offspring if not ind.fitness.valid]
fitnesses = toolbox.map(toolbox.evaluate, invalid_ind)
for ind, fit in zip(invalid_ind, fitnesses):
ind.fitness.values = fit

# Select the next generation population
pop = pop + offspring
fronts = toolbox.sort(pop, len(pop))
chosen = []
for i, front in enumerate(fronts):
# Move is front to chosen population til it is almost full
if len(chosen) + len(front) <= MU:
chosen.extend(front)
else:
# Assign hypervolume contribution to individuals of front that
# cannot be completely move over to chosen individuals
fitness_hv = hypervolume_contrib(front)
for ind, fit_hv in zip(front, fitness_hv):
ind.fitness_hv.values = (fit_hv,)
# Fill chosen with best indiviuals from inspect front
# (based on hypervolume contribution)
chosen.extend(toolbox.select(front, MU - len(chosen)))
break

pop = chosen

record = stats.compile(pop)
logbook.record(gen=gen, evals=len(invalid_ind), **record)
print(logbook.stream)

print("Final population hypervolume is %f" % hypervolume(pop, [11.0, 11.0]))

return pop, logbook

if __name__ == "__main__":
# with open("pareto_front/zdt1_front.json") as optimal_front_data:
# optimal_front = json.load(optimal_front_data)
# Use 500 of the 1000 points in the json file
# optimal_front = sorted(optimal_front[i] for i in range(0, len(optimal_front), 2))

pop, stats = main()
# pop.sort(key=lambda x: x.fitness.values)

# print(stats)
# print("Convergence: ", convergence(pop, optimal_front))
# print("Diversity: ", diversity(pop, optimal_front[0], optimal_front[-1]))

# import matplotlib.pyplot as plt
# import numpy

# front = numpy.array([ind.fitness.values for ind in pop])
# optimal_front = numpy.array(optimal_front)
# plt.scatter(optimal_front[:,0], optimal_front[:,1], c="r")
# plt.scatter(front[:,0], front[:,1], c="b")
# plt.axis("tight")
# plt.show()

0 comments on commit 98d44fc

Please sign in to comment.