Repository: csxeba/evolute Branch: master Commit: ea868e5d04e6 Files: 30 Total size: 40.6 KB Directory structure: gitextract_mywosglh/ ├── .gitignore ├── LICENSE ├── Readme.md ├── evolute/ │ ├── __init__.py │ ├── evaluation/ │ │ ├── __init__.py │ │ ├── fitness.py │ │ └── grade.py │ ├── initialization/ │ │ ├── __init__.py │ │ └── initializer.py │ ├── operators/ │ │ ├── __init__.py │ │ ├── mate.py │ │ ├── mutate.py │ │ ├── operators.py │ │ └── selection.py │ ├── population/ │ │ ├── __init__.py │ │ ├── genetic.py │ │ └── population.py │ └── utility/ │ ├── __init__.py │ ├── describe.py │ ├── history.py │ ├── keras_utility.py │ └── test_utils.py ├── examples/ │ ├── xp_evolve_net.py │ ├── xp_keras.py │ ├── xp_quadratic.py │ └── xp_simple.py ├── requirements.txt ├── setup.py └── tests/ ├── test_evaluation.py └── test_operator.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Created by .ignore support plugin (hsz.mobi) ### Python template # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ # PyCharm .idea/ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Csaba Gor Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Readme.md ================================================ [![codebeat badge](https://codebeat.co/badges/f72db301-fd66-4c05-b1ca-9b8c8196f06e)](https://codebeat.co/projects/github-com-csxeba-evolute-master) # Evolute Evolutionary algorithm toolbox ## Documentation **Evolute** is a simple tool for quick experimentation with evolutionary algorithms for numerical optimization. It defines a **population** of individuals, represented as floating point vectors, and applies a configurable set of evolutionary **operators** to them in a predefined order. The order is the following: 1. **Selection**: a subset of individuals are discarded, depending on their fitness value 2. **Reproduction**: the discarded individuals are replaced by new individuals, somehow generated from the survivors 3. **Mutation**: mutate some of the individuals 4. **Update**: update the fitnesses of the individuals Some nomenclature: - **individual**: a single member of the population. - **genotype**: refers to an individual in the encoded state. *Evolute* encodes information in floating point vectors. - **locus**: a member of a genotype (a single scalar in the vector) - **phenotype**: refers to an individual in the decoded state. In the case of neuroevolution, this would be a neural network object, which is encoded as a genotype. Evolute is not doing any *genotype - phenotype* conversion, this has to be implemented by the user in the fitness calculation. ### Module: *population* Defines containers for individuals and aggregates the operators applied to them. Currently the following population types are defined: - **GeneticPopulation**: can be used for genetic algorithms, simply evaluates the fitnesses and applies the operators in the above specified order. Its constructor accepts the following arguments: - **loci**: number of elements in an individual's chromosome - **fitness_wrapper**: instance of a class in evolute.evaluation - **limit**: maximum number of individuals, defaults to 100 - **operators**: an instance of *evolute.operators.Operators*, see operators later - **initializer**: instance of a class defined in evolute.initialization, optional Will add support for MemeticPopulation, which allows for explicit modification of the individuals during fitness calculation An evolutionary optimization can be run using the population.run() method, which accepts the following arguments: - **epochs**: how many iterations should be done - **survival_rate**: reset survival rate - **mutation_rate**: reset mutation rate - **force_update_at_every**: at every *k*th iteration, force a complete fitness-recalculation - **verbosity**: 0 is silent, > 0 is verbose. - **history**: evolute.utils.history.History object, in which runtime metrics can be recorded. If omitted, one is instantiated implicitly. The run() method returns a history object with generation, best_grade, mean_grade and grade_stdev recorded by default. A population can be saved and loaded with the save() and load() methods, which produces a gzip-compressed pickle of the Population object. Single epoch can be also run with the epoch() method and an update can be forced with the update() method. For convenience, these methods are defined: - **epoch()**: use this to evaluate a single epoch - **update()**: forces a fitness update - **simple_fitness()**: class factory method, which takes a nude fitness function and wraps it implicitly with SimpleFitness (see below). - **get_individual(index)** - **set_indivudual(index, individual)** - **get_best()**: the current best individual with the lowest fitness/grade - **get_champion**: the all-time best individual is alway stored implicitly, and can be accessed here. ### Module: *evaluation* Defines different wrappers for fitness functions. **Fitness** is a scalar value assigned to every individual. Currently in Evolute, the lower fitness is the better. Since a fitness function can be any kind of logic, fitness wrappers expect a function pointer or some kind of callable. Some advanced wrappers are there to support possibly multiple fitness functions or functions with multiple return values. In the end, the fitness has to be reduced to a single scalar. The process of combining multiple fitnesses to a single scalar is termed **grading** in Evolute. #### Fitness wrappers Currently the following fitness wappers are defined in *evolute.evaluation*: - **SimpleFitness**: wraps a simple function with a single scalar return value. Its constructor expects the following arguments: - **fitness_function**: a function reference or callable object - **constants**: optional dictionary of constants to be passed by name Variables may be passed to the fitness function during the running of the algorithm. - **MultipleFitnesses**: wraps multiple fitness functions. The constructor expects the following arguments: - **functions_by_name**: dictionary of fitness function references or callable objects. They need to be identified by a unique name or ID - **contants_by_function_name**: optional dictionary of arguments (as dicts as well). They have to be referenced by the same name or ID. - **order_by_name**: optional, an ordered iterable of the IDs if call order matters. - **grader**: grader object or any callable which takes the array of fitnesses and produces a single scalar grade from them. Defaults to simple summation. - **MultiReturnFitness**: wrapper for a single fitness function which returns multiple fitness values. The constructor expects the following arguments: - **number_of_return_values** - **constants**: optional - **grader**: optional, defaults to simple summation #### Graders More advanced fitness wrappers expect a Grader instance defined in *evolute.evaluation.grade* or any callable which accepts a NumPy array and returns a single scalar. Some graders are available in *evolute.evaluation*: - **SumGrader**: simply sums the fitness values - **WeightedSumGrader**: accepts a set of weights in its constructor and produces a weighted sum (dot product) with the fitness values. ### Module: *initialization* This module defines initializers: objects for random population initialization strategies. Currently the following distributions are available in *evolute.initialization*: - **NormalRandom**: with customizable **mean** and **stdev** - **UniformRandom**: with customizable **low** and **high** - **OrthogonalNormal**: produces a diagonal random matrix ### Module: *operators* Evolutionary operators are defined here. This module defines an **Operators** class, which aggregates all the operators needed to run an evolutionary optimization. Operators' constructor takes the following arguments: - **selection_op** - **mutate_op** - **mate_op** Submodules define the particular operator types, which are the following: #### Selection During selection, a subset of individuals are discarded. The logic by which these are specified depends on the actual implementation: - **Elitism**: selects the top m% of individuals. Its constructor accepts the following arguments: - **selection_rate**: scalar between 0 and 1 to specify the rate of discarded individuals - **mate_op**: a mate operator, which is used to fill up the slots where individuals are missing (applies reproduction). Details on these later. - **exclude_self_mating**: bool, whether to disallow mating an individual with itself If no selection operator is defined, every selection-using class defaults to *Elitism*. #### Mate Mating is defined as some kind of combination of two individuals to reproduce a new individual. Currently the following mate operators are defined: - **LambdaMate**: wraps a user-defined function, which takes two individuals and produces a single individual - **RandomPickMate**: randomly pics loci from the two genotypes - **SmoothMate**: takes the mean of the two genotypes - **ScatterMateWrapper**: wrapper for any mate operator. When applied, it adds gaussian noise to the new individual. Its constructor expects the following parameters: - **base**: reference to the base mate operator object - **stdev**: standard deviation of the additive gaussian noise If no mate operator is defined, every mate-using class defaults to *RandomPickMate*. #### Mutate This operator takes the whole population and mutates its individuals by perturbing them with some kind of noise. The following mutation operators are available: - **UniformLocuswiseMutation**: adds uniform distributed noise to individuals. Mutants are determined on the locus level, and mutation rate (see below) is adjusted for this. Its constructor takes the following arguments: - **rate**: rate by which mutations occur in the population. It has to be given as a rate of individuals but it is implicitly corrected for loci. - **low** and **high**: parameters of the uniform distribution - **NormalIndividualwiseMutation**: adds normal random noise to individuals. Mutants are determined on the individuals' level. Its constructor takes the following arguments: - **rate** - **stdev** If no mutate operator is defined, every mutate-using class defaults to *UniformLocuswiseMutation*. ### Module: utility Some useful stuff here. Maybe the most interesting is the *evolute.keras_utility* module, which defines some helpers to interface with Keras and do some Neuroevolution on the weights of a network. ================================================ FILE: evolute/__init__.py ================================================ from .population import GeneticPopulation ================================================ FILE: evolute/evaluation/__init__.py ================================================ from .fitness import FitnessBase, SimpleFitness, MultipleFitnesses, MultiReturnFitness from .grade import GraderBase, SumGrader, WeightedSumGrader ================================================ FILE: evolute/evaluation/fitness.py ================================================ import numpy as np from .grade import SumGrader class FitnessBase: def __init__(self, no_fitnesses): self.no_fitnesses = no_fitnesses def __call__(self, phenotype): raise NotImplementedError class SimpleFitness(FitnessBase): def __init__(self, fitness_function, constants: dict=None, **kw): super().__init__(no_fitnesses=1) self.function = fitness_function self.constants = {} if constants is None else constants self.constants.update(kw) def __call__(self, phenotype, **variables): return self.function(phenotype, **self.constants, **variables) class MultipleFitnesses(FitnessBase): def __init__(self, functions_by_name, constants_by_function_name=None, order_by_name=None, grader=None): super().__init__(no_fitnesses=len(functions_by_name)) if len(functions_by_name) < 2: raise ValueError("MultipleFitnesses needs more than one fitness!") self.functions = functions_by_name self.order = order_by_name or list(self.functions) self.constants = constants_by_function_name or {k: {} for k in self.order} self.grader = grader or SumGrader() if len(self.order) != len(self.functions) or any(o not in self.functions for o in self.order): raise ValueError("The specified order is wrong: {}".format(self.order)) if len(self.constants) != len(self.functions) or any(k not in self.functions for k in self.constants): raise ValueError("The specified constants are wrong: {}".format(self.constants)) def __call__(self, phenotype, **variables_by_function): fitness = np.array( [self.functions[funcname](phenotype, **self.constants[funcname], **variables_by_function[funcname]) for funcname in self.order]) return self.grader(fitness) class MultiReturnFitness(FitnessBase): def __init__(self, fitness_function, number_of_return_values, constants: dict=None, grader=None): super().__init__(no_fitnesses=number_of_return_values) self.function = fitness_function self.constants = {} if constants is None else constants self.grader = SumGrader() if grader is None else grader def __call__(self, phenotype, **variables): fitness = np.array(self.function(phenotype, **self.constants, **variables)) return self.grader(fitness) ================================================ FILE: evolute/evaluation/grade.py ================================================ import numpy as np class GraderBase: def __call__(self, fitness): raise NotImplementedError class SumGrader(GraderBase): def __call__(self, fitness): return np.sum(fitness) class WeightedSumGrader(GraderBase): def __init__(self, weights): self.weights = np.ones(1) if weights is None else weights def __call__(self, fitness): return np.dot(fitness, self.weights) ================================================ FILE: evolute/initialization/__init__.py ================================================ from .initializer import NormalRandom, UniformRandom, OrthogonalNormal DefaultInitializer = NormalRandom ================================================ FILE: evolute/initialization/initializer.py ================================================ import numpy as np class Initializer: def initialize(self, *shape): raise NotImplementedError class NormalRandom(Initializer): def __init__(self, mean=0., stdev=1.): self.mean = mean self.stdev = stdev def initialize(self, *shape): return np.random.randn(*shape) * self.stdev + self.mean class UniformRandom(Initializer): def __init__(self, low=-1, high=1.): self.low = low self.high = high def initialize(self, *shape): return np.random.uniform(self.low, self.high, shape) class OrthogonalNormal(Initializer): def initialize(self, *shape): individials = NormalRandom().initialize(shape) d, V = np.linalg.eig(np.cov(individials.T)) D = np.diag(1. / np.sqrt(d + 1e-7)) W = V @ D @ V.T return individials @ W ================================================ FILE: evolute/operators/__init__.py ================================================ from .mate import DefaultMate, LambdaMate, SmoothMate, RandomPickMate, ScatterMateWrapper from .mutate import DefaultMutate, UniformLocuswiseMutation, NormalIndividualwiseMutation from .selection import DefaultSelection, Elitism from .operators import Operators ================================================ FILE: evolute/operators/mate.py ================================================ import numpy as np class MateBase: def apply(self, ind1, ind2): pass def __call__(self, ind1, ind2): return self.apply(ind1, ind2) class LambdaMate(MateBase): def __init__(self, function_ref, **kw): self.kwargs = kw self.apply = lambda ind1, ind2: function_ref(ind1, ind2, **self.kwargs) class RandomPickMate(MateBase): def apply(self, ind1, ind2): return np.where(np.random.uniform(size=ind1.shape) < 0.5, ind1, ind2) class SmoothMate(MateBase): def apply(self, ind1, ind2): return np.mean((ind1, ind2), axis=0) DefaultMate = RandomPickMate class ScatterMateWrapper(MateBase): def __init__(self, base=DefaultMate, stdev=1.): if isinstance(base, type): base = base() self.base = base self.stdev = stdev def apply(self, ind1, ind2): return self.base(ind1, ind2) + np.random.randn(*ind1.shape) * self.stdev ================================================ FILE: evolute/operators/mutate.py ================================================ import numpy as np class MutationBase: def __init__(self, rate=0.1): self.rate = rate self.mask = None def set_rate(self, rate): if rate < 0. or rate > 1.: raise ValueError("Mutation rate has to be >= 0 and <= 1") self.rate = rate def apply(self, individuals, inplace=False): raise NotImplementedError def __call__(self, individuals, inplace=False): return self.apply(individuals) class UniformLocuswiseMutation(MutationBase): def __init__(self, rate=0.1, low=-1., high=1.): super().__init__(rate) self.low = low self.high = high def set_params(self, low=None, high=None): self.low = self.low if low is None else low self.high = self.high if high is None else high def apply(self, individuals, inplace=False): indshape = individuals.shape if self.rate == 0.: self.mask = np.zeros(len(individuals), dtype=bool) return individuals elif self.rate == 1.: mask = np.ones(indshape, dtype=bool) else: mask = np.random.uniform(size=indshape) < (self.rate / indshape[-1]) self.mask = np.any(mask, axis=1) noise = np.random.uniform(self.low, self.high, size=mask.sum()) if not inplace: mutants = individuals.copy() mutants[mask] += noise return mutants individuals[mask] += noise class NormalIndividualwiseMutation(MutationBase): def __init__(self, rate=0.1, stdev=1.): super().__init__(rate) self.std = stdev def set_param(self, stdev): self.std = stdev def apply(self, individuals, inplace=False): mask = np.random.uniform(size=len(individuals)) < self.rate noise = np.random.normal(loc=0., scale=self.std, size=(mask.sum(), individuals.shape[-1])) if not inplace: mutants = individuals.copy() mutants[mask] += noise return mutants individuals[mask] += noise self.mask = mask DefaultMutate = UniformLocuswiseMutation ================================================ FILE: evolute/operators/operators.py ================================================ import numpy as np from . import DefaultSelection, DefaultMutate, DefaultMate class Operators: def __init__(self, selection_op=None, mutate_op=None, mate_op=None): self.selection = DefaultSelection() if selection_op is None else selection_op self.mutation = DefaultMutate() if mutate_op is None else mutate_op self._clarify_mate_operator(mate_op) def _clarify_mate_operator(self, mate_op): mate_set_in_selection = self.selection.mate_op is not None mate_set_here = mate_op is not None if mate_set_in_selection and mate_set_here: print(" [w] Evolute: differring mate ops, using the one in Selection!") elif mate_set_in_selection and not mate_set_here: pass elif not mate_set_in_selection and mate_set_here: self.selection.set_mate_operator(mate_op) elif not mate_set_in_selection and not mate_set_here: self.selection.set_mate_operator(DefaultMate()) else: assert False, "O.o" # w00t def invalid_individual_indices(self): return np.where(self.selection.mask | self.mutation.mask)[0] ================================================ FILE: evolute/operators/selection.py ================================================ import numpy as np class SelectionBase: def __init__(self, selection_rate=0.5, mate_op=None, exclude_self_mating=True): self.mate_op = mate_op self.rate = None self._selection_mask = None self.exclude_self_mating = exclude_self_mating self.set_selection_rate(selection_rate) @property def mask(self): return self._selection_mask def set_mate_operator(self, mate_op): self.mate_op = mate_op def _stream_of_parent_indices(self): assert self._selection_mask is not None survivor_mask = ~self._selection_mask arg1 = np.argwhere(survivor_mask)[:, 0] assert len(arg1) > 1 arg2 = np.copy(arg1) limit = sum(self._selection_mask) n = 0 while n < limit: np.random.shuffle(arg1) np.random.shuffle(arg2) for ix1, ix2 in zip(arg1, arg2): if n >= limit: raise StopIteration if self.exclude_self_mating and ix1 == ix2: continue yield ix1, ix2 n += 1 def set_survival_rate(self, survival_rate): if survival_rate <= 0. or survival_rate > 1.: raise ValueError("The rate of survival has to be greater than 0 and less or equal to 1") self.rate = survival_rate def set_selection_rate(self, selection_rate): if selection_rate <= 0. or selection_rate > 1.: raise ValueError("The rate of selection has to be greater than 0 and less or equal to 1") self.rate = 1. - selection_rate def apply(self, individuals, fitnesses, inplace=False): raise NotImplementedError def __call__(self, individuals, fitnesses, inplace=False): return self.apply(individuals, fitnesses, inplace) class Elitism(SelectionBase): def apply(self, individuals, fitnesses, inplace=False): self._selection_step(individuals, fitnesses) if not inplace: return self._reproduction_copy(individuals) self._reproduction_inplace(individuals) def _selection_step(self, individuals, grades): limit, loci = individuals.shape self._selection_mask = np.ones(limit, dtype=bool) if self.rate: no_survivors = max(2, int(limit * self.rate)) survivors = np.argsort(grades)[:no_survivors] self._selection_mask[survivors] = False def _reproduction_inplace(self, individuals): individuals[self._selection_mask] = [ self.mate_op(individuals[idx1], individuals[idx2]) for idx1, idx2 in self._stream_of_parent_indices() ] def _reproduction_copy(self, individuals): offspring = individuals.copy() new_indivs = [ self.mate_op(offspring[idx1], offspring[idx2]) for idx1, idx2 in self._stream_of_parent_indices() ] offspring[self._selection_mask] = new_indivs return offspring DefaultSelection = Elitism ================================================ FILE: evolute/population/__init__.py ================================================ from .population import Population from .genetic import GeneticPopulation ================================================ FILE: evolute/population/genetic.py ================================================ from .population import Population class GeneticPopulation(Population): def update_individual(self, index, **fitness_kw): self.fitnesses[index] = self.fitness(self.get_individual(index), **fitness_kw) ================================================ FILE: evolute/population/population.py ================================================ import numpy as np from ..initialization import DefaultInitializer from ..operators import Operators from ..evaluation import SimpleFitness from ..utility.history import History class Population: def __init__(self, loci: int, fitness_wrapper, limit=100, operators=None, initializer=None): """ :param loci: number of elements in an individual's chromosome :param fitness_wrapper: accepts an individual, returns fitnesses :param limit: maximum number of individuals :param operators: an instance of Operators :param initializer: instance of a class defined in evolute.initialization and index of mutants """ self.loci = loci self.limit = limit self.fitness = fitness_wrapper self.fitnesses = None self.operators = Operators() if operators is None else operators self.initializer = DefaultInitializer() if initializer is None else initializer self.individuals = self.initializer.initialize(self.limit, self.loci) self.age = 0 self.champion = 0 @classmethod def simple_fitness(cls, fitness_callback, loci, limit=100, initializer=None, operators=None, fitness_constants=None): fitness_wrapper = SimpleFitness(fitness_callback, {} if fitness_constants is None else fitness_constants) return cls(loci=loci, fitness_wrapper=fitness_wrapper, limit=limit, initializer=initializer, operators=operators) def get_individual(self, index): return self.individuals[index] def set_individual(self, index, individual): self.individuals[index] = individual def get_best(self): return self.get_individual(np.argmin(self.fitnesses)) def get_champion(self): return self.get_individual(self.champion) def get_fitness_weighted_average_individual(self): weights = (self.fitnesses - self.fitnesses.mean()) / self.fitnesses.std() return weights @ self.individuals def run(self, epochs: int, survival_rate: float=0.5, mutation_rate: float=0.1, force_update_at_every: int=0, verbosity: int=1, history=None): """ :param epochs: number of epochs to run for :param survival_rate: 0-1, how many individuals survive the selection :param mutation_rate: 0-1, rate of mutation at each epoch :param force_update_at_every: complete reupdate at specified intervals :param verbosity: 1 is verbose, > 1 also prints out v - 1 individuals :param history: History object in which run stats should be recorded :return: history object containing run statistics """ history = (History(["generation", "best_grade", "mean_grade", "grade_std"]) if history is None else history) self.operators.selection.set_survival_rate(survival_rate) self.operators.mutation.set_rate(mutation_rate) for epoch in range(1, epochs+1): if verbosity: print("-" * 50) print("Epoch {}/{}".format(epochs, epoch)) self.epoch(force_update=force_update_at_every and epoch % force_update_at_every == 0, verbosity=verbosity) history.record({"generation": self.age, "best_grade": self.fitnesses.min(), "mean_grade": self.fitnesses.mean(), "grade_std": self.fitnesses.std()}) if verbosity: print() return history def epoch(self, force_update=False, verbosity=1, **fitness_kw): if not self.age: self._initialize(verbosity, **fitness_kw) self.operators.selection(self.individuals, self.fitnesses, inplace=True) self.individuals = self.operators.mutation(self.individuals, inplace=False) self.update(force_update, verbose=verbosity, **fitness_kw) self.age += 1 def _initialize(self, verbosity, **fitness_kw): if verbosity: print("EVOLUTION: initial update...") self.fitnesses = np.empty(self.limit) self.update(forced=True, verbose=verbosity, **fitness_kw) if verbosity: print("EVOLUTION: initial mean grade :", self.fitnesses.mean()) print("EVOLUTION: initial std of mean:", self.fitnesses.std()) print("EVOLUTION: initial best grade :", self.fitnesses.min()) def update(self, forced=False, verbose=0, **fitness_kw): inds = self._invalidated_individual_indices(force_update=forced) for ind in inds.flat: if verbose: print("\rUpdating {}/{}".format(self.limit, ind+1), end="") self.update_individual(ind, **fitness_kw) if verbose: print("\rUpdating {}/{}".format(self.limit, self.limit), end="") print(" Best grade:", self.fitnesses.min()) chump = self.fitnesses.argmin() if self.fitnesses[chump] < self.fitnesses[self.champion]: self.champion = chump def update_individual(self, index, **fitness_kw): raise NotImplementedError def _invalidated_individual_indices(self, force_update): return np.arange(self.limit) if force_update else self.operators.invalid_individual_indices() def save(self, path): import pickle import gzip with gzip.open(path, "wb") as handle: pickle.dump(self, handle) @staticmethod def load(path): import pickle import gzip return pickle.load(gzip.open(path, "rb")) ================================================ FILE: evolute/utility/__init__.py ================================================ ================================================ FILE: evolute/utility/describe.py ================================================ import numpy as np def describe(population, show=0): showme = np.argsort(population.grades)[:show] chain = "-" * 50 + "\n" shln = len(str(show)) for i, index in enumerate(showme, start=1): genomechain = ", ".join( "{:>6.4f}".format(loc) for loc in np.round(population.individuals[index], 4)) fitnesschain = "[" + ", ".join( "{:^8.4f}".format(fns) for fns in population.fitnesses[index]) + "]" chain += "TOP {:>{w}}: [{:^14}] F = {:<} G = {:.4f}\n".format( i, genomechain, fitnesschain, population.grades[index], w=shln) best_arg = population.grades.argmin() chain += "Best Grade : {:7>.4f} ".format(population.grades[best_arg]) chain += "Fitnesses: [" chain += ", ".join("{}".format(f) for f in population.fitnesses[best_arg]) chain += "]\n" chain += "Mean Grade : {:7>.4f}, STD: {:7>.4f}\n" \ .format(population.grades.mean(), population.grades.std()) print(chain) ================================================ FILE: evolute/utility/history.py ================================================ class History: def __init__(self, aspects=()): self.history = {aspect: [] for aspect in ["generation"] + list(aspects)} def record(self, data): for key in data: self.history[key].append(data[key]) def __getitem__(self, item): return self.history[item] ================================================ FILE: evolute/utility/keras_utility.py ================================================ import numpy as np def get_keras_weights(model, folded=False): w_tensors = model.trainable_weights if folded: return w_tensors return np.concatenate([w.flat for w in w_tensors]) def get_keras_number_of_trainables(model): return sum(w.size for w in model.trainable_weights) class WeightFolding: def __init__(self, model): self.shapes = [w.shape for w in model.get_weights()] self.sizes = [np.prod(shape) for shape in self.shapes] def __call__(self, individual): phenotype = [] start = 0 for shape, size in zip(self.shapes, self.sizes): end = start + size phenotype.append(individual[start:end].reshape(shape)) start = end return phenotype ================================================ FILE: evolute/utility/test_utils.py ================================================ import numpy as np def is_standardish(array, globally=False, epsilon=1e-7): if globally: return np.allclose(array.mean(), 0., atol=epsilon) and np.allclose(array.std(), 1., atol=epsilon ) return np.allclose(array.mean(axis=0), 0., atol=epsilon) and np.allclose(array.std(axis=0), 1., atol=epsilon) def is_normalish(array, epsilon=1e-7): return np.allclose(np.linalg.norm(array, axis=1), 1., atol=epsilon) ================================================ FILE: examples/xp_evolve_net.py ================================================ import numpy as np from matplotlib import pyplot as plt from brainforge import LayerStack from brainforge.layers import DenseLayer from brainforge.cost import costs from evolute.operators import ScatterMateWrapper, SmoothMate, Operators from evolute import GeneticPopulation np.random.seed(1234) rX = np.linspace(-6., 6., 200)[:, None] rY = np.sin(rX) arg = np.arange(len(rX)) np.random.shuffle(arg) targ, varg = arg[:100], arg[100:] targ.sort() varg.sort() tX, tY = rX[targ], rY[targ] vX, vY = rX[varg], rY[varg] tX += np.random.randn(*tX.shape) / np.sqrt(tX.size*0.25) loss_fn = costs["mse"] def fitness(phenotype, layerstack, X, Y): layerstack.set_weights(phenotype) return loss_fn(layerstack.predict(X), Y) def forge_layerstack(): return LayerStack(input_shape=(1,), layers=[ DenseLayer(30, activation="tanh"), DenseLayer(30, activation="tanh"), DenseLayer(1, activation="linear") ]) def get_population(): layers = forge_layerstack() operators = Operators(mate_op=ScatterMateWrapper(SmoothMate, 3.)) pop = GeneticPopulation.simple_fitness(limit=100, loci=layers.nparams, operators=operators, fitness_callback=fitness, fitness_constants={"layerstack": layers}) return layers, pop def xperiment(): layers, pop = get_population() layers = forge_layerstack() tpred = layers.predict(tX) vpred = layers.predict(vX) plt.ion() plt.plot(tX, tY, "b--", alpha=0.5, label="Training data (noisy)") plt.plot(rX, rY, "r--", alpha=0.5, label="Validation data (clean)") plt.ylim(min(rY)-1, max(rY)+1) plt.plot(rX, np.zeros_like(rX), c="grey", linestyle="--") tobj, = plt.plot(tX, tpred, "bo", markersize=3, alpha=0.5, label="Training pred") vobj, = plt.plot(vX, vpred, "ro", markersize=3, alpha=0.5, label="Validation pred") templ = "Batch: {:>5} Cost = {:.4f}" t = plt.title(templ.format(0, 0)) plt.legend() batchno = 1 while 1: pop.epoch(X=tX, Y=tY) layers.set_weights(pop.get_champion()) tpred = layers.predict(tX) vpred = layers.predict(vX) tobj.set_data(tX, tpred) vobj.set_data(vX, vpred) plt.pause(0.01) t.set_text(templ.format(batchno, pop.fitnesses.min())) batchno += 1 if __name__ == '__main__': xperiment() ================================================ FILE: examples/xp_keras.py ================================================ import numpy as np from keras.layers import Dense from keras.models import Sequential from keras.datasets import mnist from keras.utils import to_categorical from evolute import GeneticPopulation from evolute.evaluation import SimpleFitness from evolute.initialization import NormalRandom from evolute.operators import RandomPickMate, Operators, UniformLocuswiseMutation, ScatterMateWrapper from evolute.utility.keras_utility import WeightFolding def pull_mnist(): learning, testing = mnist.load_data() Xs, Ys = (learning[0], testing[0]), (learning[1], testing[1]) Xs = map(lambda x: (x.reshape(-1, 784) - 127.5) / 255., Xs) Ys = map(lambda y: to_categorical(y, num_classes=10), Ys) return tuple(Xs), tuple(Ys) def fitness_callback(phenotype, model: Sequential, w_folder, X, Y): model.set_weights(w_folder(phenotype)) cost, acc = model.evaluate(X, Y, verbose=0) return cost (lX, tX), (lY, tY) = pull_mnist() ann = Sequential([ Dense(64, activation="tanh", input_dim=lX.shape[1]), Dense(lY.shape[1], activation="softmax") ]) ann.compile(optimizer="sgd", loss="categorical_crossentropy", metrics=["acc"]) w_shapes = [w.shape for w in ann.get_weights()] w_flat = np.concatenate([w.flat for w in ann.get_weights()]) w_folder = WeightFolding(ann) fitness = SimpleFitness(fitness_function=fitness_callback, constants={"model": ann, "w_folder": w_folder}) population = GeneticPopulation( limit=100, loci=w_flat.size, fitness_wrapper=fitness, initializer=NormalRandom(mean=w_flat), operators=Operators(mate_op=ScatterMateWrapper(RandomPickMate(), stdev=2.), mutate_op=UniformLocuswiseMutation(low=-3., high=3.)) ) BATCH_SIZE = 128 batch_stream = ((lX[start:start+BATCH_SIZE], lY[start:start+BATCH_SIZE]) for start in range(0, len(lX), BATCH_SIZE)) population.operators.selection.set_selection_rate(0.98) population.operators.mutation.set_rate(0.0) for i, (x, y) in enumerate(batch_stream, start=1): population.epoch(X=x, Y=y, verbosity=0) ann.set_weights(w_folder(population.get_best())) cost, acc = ann.evaluate(tX, tY, verbose=0) print("\rBatch: {} Acc: {:.2%}, Cost: {:.4f}".format(i, acc, cost)) ================================================ FILE: examples/xp_quadratic.py ================================================ import numpy as np from matplotlib import pyplot as plt from evolute import GeneticPopulation from evolute.evaluation import SimpleFitness def fitness(ind, target): return np.linalg.norm(target - ind) def main(): TARGET = np.array([3., 3.]) pop = GeneticPopulation( loci=2, fitness_wrapper=SimpleFitness(fitness, constants={"target": TARGET}), limit=100) plt.ion() obj = plt.plot(*pop.individuals.T, "bo", markersize=2)[0] plt.xlim([-2, 11]) plt.ylim([-2, 11]) X, Y = np.linspace(-2, 11, 50), np.linspace(-2, 11, 50) X, Y = np.meshgrid(X, Y) Z = np.array([fitness(np.array([x, y]), target=TARGET) for x, y in zip(X.ravel(), Y.ravel())]).reshape(X.shape) CS = plt.contour(X, Y, Z, cmap="hot") plt.clabel(CS, inline=1, fontsize=10) title_template = "Best: [{:.4f}, {:.4f}], G: {:.4f}" title_obj = plt.title(title_template.format(0., 0., 0.)) plt.show() means, stds, bests = [], [], [] for i in range(30): pop.epoch(force_update=True, verbosity=0) means.append(pop.fitnesses.mean()) stds.append(pop.fitnesses.std()) bests.append(pop.fitnesses.min()) obj.set_data(*pop.individuals.T) title_obj.set_text(title_template.format(*pop.get_best(), pop.fitnesses.min())) plt.pause(0.1) means, stds, bests = tuple(map(np.array, (means, stds, bests))) plt.close() plt.ioff() Xs = np.arange(1, len(means) + 1) plt.plot(Xs, means, "b-", label="mean") plt.plot(Xs, means+stds, "g--", label="stdev") plt.plot(Xs, means-stds, "g--") plt.plot(Xs, bests, "r-", label="best") plt.xlim([Xs.min()-1, Xs.max()+1]) plt.ylim([bests.min()-1, (means+stds).max()+1]) plt.legend() plt.grid() plt.show() if __name__ == '__main__': main() ================================================ FILE: examples/xp_simple.py ================================================ import numpy as np from matplotlib import pyplot as plt from evolute import GeneticPopulation from evolute.evaluation import SimpleFitness TARGET = np.ones(10) * 0.5 pop = GeneticPopulation(loci=10, fitness_wrapper=SimpleFitness(lambda ind: np.linalg.norm(ind - TARGET))) history = pop.run(100) history = {k: np.array(v) for k, v in history.history.items()} x = history["generation"] plt.plot(x, history["mean_grade"], "r-", label="mean") plt.plot(x, history["mean_grade"] + history["grade_std"], "b--", label="std") plt.plot(x, history["mean_grade"] - history["grade_std"], "b--") plt.plot(x, history["best_grade"], "g-", label="mean") plt.title("Population convergence") plt.legend() plt.grid() plt.show() ================================================ FILE: requirements.txt ================================================ numpy ================================================ FILE: setup.py ================================================ from setuptools import setup, find_packages setup( name='evolute', version='0.9.0', packages=find_packages(), url='https://github.com/csxeba/evolute.git', license='MIT', author='Csaba Gór', author_email='csxeba@gmail.com', description='Evolutionary algorithm toolbox', long_description=open("Readme.md").read(), long_description_content_type='text/markdown' ) ================================================ FILE: tests/test_evaluation.py ================================================ import unittest import numpy as np from evolute.evaluation import WeightedSumGrader class TestGrade(unittest.TestCase): def setUp(self): self.sample_fitness_vector = np.ones(3) self.sample_fitness_weigts = np.ones(3) + 1 def test_weighted_sum_grader(self): grader = WeightedSumGrader(weights=self.sample_fitness_weigts) calced = grader(self.sample_fitness_vector) self.assertEqual(calced, self.sample_fitness_vector @ self.sample_fitness_weigts) ================================================ FILE: tests/test_operator.py ================================================ import unittest import numpy as np from evolute.operators import SmoothMate, RandomPickMate from evolute.operators import UniformLocuswiseMutation from evolute.operators import Elitism class TestMate(unittest.TestCase): def setUp(self): self.sample_individuals = [ np.zeros(3), np.ones(3) + 1 ] def test_random_pick_mated_offspring_only_contains_entries_from_parents(self): mater = RandomPickMate() offspring = mater(*self.sample_individuals) eq = np.logical_or(offspring == 0., offspring == 2.) self.assertTrue(np.all(eq)) def test_smooth_mate_produces_offspring_which_is_the_mean_of_parents(self): mater = SmoothMate() offspring = mater(*self.sample_individuals) self.assertTrue(np.all(offspring == np.ones_like(offspring))) class TestMutate(unittest.TestCase): def setUp(self): self.sample_individuals = np.zeros((3, 4)) def test_uniform_locuswise_op_mutates_every_locus_with_rate_1(self): # Test may fail in the very unlikely case of a mutation perturbance element being exactly 0. mutator = UniformLocuswiseMutation(rate=1.) mutant = mutator(self.sample_individuals) self.assertFalse(np.all(mutant == self.sample_individuals)) class TestSelection(unittest.TestCase): def setUp(self): self.sample_individuals = np.stack([np.zeros(3)]*9 + [np.ones(3) + 1.], axis=0) self.sample_grades = np.arange(len(self.sample_individuals), 0, -1) def test_number_of_selected_individuals_corresponds_to_set_selection_rate(self): rate = 0.5 selector = Elitism(selection_rate=rate) selector(self.sample_individuals, self.sample_grades) self.assertEqual(selector.mask.sum(), rate * len(self.sample_individuals)) def test_elitism_with_an_engineered_population(self): rate = 0.8 no_offsprings = int(rate * 10) selector = Elitism(selection_rate=rate, mate_op=SmoothMate(), exclude_self_mating=True) offspring = selector(self.sample_individuals, self.sample_grades, inplace=False) self.assertTrue(np.all(offspring[:no_offsprings] == 1.))