Package: ruck
🧬 Ruck 🏉Performant evolutionary algorithms for PythonVisit theexamplesto get started, or browse thereleases.Contributionsare welcome!GoalsRuck aims to fill the following criteria:Providehigh quality,readableimplementations of algorithms.Be easilyextensibleanddebuggable.Performant while maintaining its simplicity.FeaturesRuck has various features that will be expanded upon in time📦 Modular evolutionary systems inspired by pytorch lightningHelps organise code & arguably looks clean➕ Multi-Objective optimisation supportOptionally optimised version of NSGA-II ifnumbais installed, over 65x faster🏎 Optional multithreading support withray, including helper functions🏭 Factory methods for simple evolutionary algorithms🧪 Various helper functions for selection, mutation and matingCiting RuckPlease use the following citation if you use Ruck in your research:@Misc{Michlo2021Ruck,author={Nathan Juraj Michlo},title={Ruck - Performant evolutionary algorithms for Python},howpublished={Github},year={2021},url={https://github.com/nmichlo/ruck}}OverviewRuck takes inspiration from PyTorch Lightning's module system. The population creation,
offspring, evaluation and selection steps are all contained within a single module inheriting
fromEaModule. While the training logic and components are separated into its own class.Membersof aPopulation(A list of Members) are intended to be read-only. Modifications should not
be made to members, instead new members should be created with the modified values. This enables us to
easily implement efficient multi-threading, see below!The trainer automatically constructsHallOfFameandLogBookobjects which keep track of your
population and offspring.EaModuleprovides defaults forget_stats_groupsandget_progress_statsthat can be overridden if you wish to customize the tracked statistics and statistics displayed by tqdm.Minimal OneMax Exampleimportrandomimportnumpyasnpfromruckimport*classOneMaxMinimalModule(EaModule):"""Minimal onemax example- The goal is to flip all the bits of a boolean array to True- Offspring are generated as bit flipped versions of the previous population- Selection tournament is performed between the previous population and the offspring"""# evaluate unevaluated members according to their valuesdefevaluate_values(self,values):return[v.sum()forvinvalues]# generate 300 random members of size 100 with 50% bits flippeddefgen_starting_values(self):return[np.random.random(100)<0.5for_inrange(300)]# randomly flip 5% of the bits of each each member in the population# the previous population members should never be modifieddefgenerate_offspring(self,population):return[Member(m.value^(np.random.random(m.value.shape)<0.05))forminpopulation]# selection tournament between population and offspringdefselect_population(self,population,offspring):combined=population+offspringreturn[max(random.sample(combined,k=3),key=lambdam:m.fitness)for_inrange(len(population))]if__name__=='__main__':# create and train the populationmodule=OneMaxMinimalModule()pop,logbook,halloffame=Trainer(generations=100,progress=True).fit(module)print('initial stats:',logbook[0])print('final stats:',logbook[-1])print('best member:',halloffame.members[0])Advanced OneMax ExampleRuck provides various helper functions and implementations of evolutionary algorithms for convenience.
The following example makes use of these additional features so that components and behaviour can
easily be swapped out.The three basic evolutionary algorithms provided are based arounddeap'sdefault algorithms fromdeap.algorithms. These basic evolutionary algorithms can be created fromruck.functional.make_ea. We provide the aliasruck.Rforruck.functional.R.make_easupports
the following modes:simple,mu_plus_lambdaandmu_comma_lambda.Code Example"""OneMax serial example based on:https://github.com/DEAP/deap/blob/master/examples/ga/onemax_numpy.py"""importfunctoolsimportnumpyasnpfromruckimport*classOneMaxModule(EaModule):def__init__(self,population_size:int=300,offspring_num:int=None,# offspring_num (lambda) is automatically set to population_size (mu) when `None`member_size:int=100,p_mate:float=0.5,p_mutate:float=0.5,ea_mode:str='simple'):# save the arguments to the .hparams property. values are taken from the# local scope so modifications can be captured if the call to this is delayed.self.save_hyperparameters()# implement the required functions for `EaModule`self.generate_offspring,self.select_population=R.make_ea(mode=self.hparams.ea_mode,offspring_num=self.hparams.offspring_num,mate_fn=R.mate_crossover_1d,mutate_fn=functools.partial(R.mutate_flip_bit_groups,p=0.05),select_fn=functools.partial(R.select_tournament,k=3),p_mate=self.hparams.p_mate,p_mutate=self.hparams.p_mutate,)defevaluate_values(self,values):returnmap(np.sum,values)defgen_starting_values(self)->Population:return[np.random.random(self.hparams.member_size)<0.5foriinrange(self.hparams.population_size)]if__name__=='__main__':# create and train the populationmodule=OneMaxModule(population_size=300,member_size=100)pop,logbook,halloffame=Trainer(generations=40,progress=True).fit(module)print('initial stats:',logbook[0])print('final stats:',logbook[-1])print('best member:',halloffame.members[0])Multithreading OneMax Example (Ray)If we need to scale up the computational requirements, for example requiring increased
member and population sizes, the above serial implementations will soon run into performance problems.The basic Ruck implementations of various evolutionary algorithms are designed around amapfunction that can be swapped out to add multi-threading support. We can easily do this usingrayand we even provide various helper functions that
enhance ray support.We begin by placing member's values into shared memory using ray's read-only object store
and theray.putfunction. TheseObjectRef'svalues point to the originalnp.ndarrayvalues. When retrieved withray.getthey obtain the original
arrays using an efficient zero-copy procedure. This is advantageous over something like Python's multiprocessing module which uses
expensive pickle operations to pass data around.The second step is to swap out the aforementionedmapfunction in the previous example to a
multiprocessing equivalent. We useray.remotealong withray.get, and provide theray_mapfunction
that has the same API as python map, but acceptsray.remote(my_fn).remotevalues instead.Finally we need to update ourmateandmutatefunctions to handleObjectRefs, we provide a convenient
wrapper to automatically callray.puton function results so that you can re-use your existing code.Code Example"""OneMax parallel example using ray's object store.8 bytes * 1_000_000 * 128 members ~= 128 MB of memory to store this population.This is quite a bit of processing that needs to happen! But using rayand its object store we can do this efficiently!"""fromfunctoolsimportpartialimportnumpyasnpfromruckimport*fromruck.external.rayimport*classOneMaxRayModule(EaModule):def__init__(self,population_size:int=300,offspring_num:int=None,# offspring_num (lambda) is automatically set to population_size (mu) when `None`member_size:int=100,p_mate:float=0.5,p_mutate:float=0.5,ea_mode:str='mu_plus_lambda'):self.save_hyperparameters()# implement the required functions for `EaModule`self.generate_offspring,self.select_population=R.make_ea(mode=self.hparams.ea_mode,offspring_num=self.hparams.offspring_num,# decorate the functions with `ray_remote_put` which automatically# `ray.get` arguments that are `ObjectRef`s and `ray.put`s returned resultsmate_fn=ray_remote_puts(R.mate_crossover_1d).remote,mutate_fn=ray_remote_put(R.mutate_flip_bit_groups).remote,# efficient to compute locallyselect_fn=partial(R.select_tournament,k=3),p_mate=self.hparams.p_mate,p_mutate=self.hparams.p_mutate,# ENABLE multiprocessingmap_fn=ray_map,)# eval function, we need to cache it on the class to prevent# multiple calls to ray.remote. We use ray.remote instead of# ray_remote_put like above because we want the returned values# not object refs to those values.self._ray_eval=ray.remote(np.mean).remotedefevaluate_values(self,values):# values is a list of `ray.ObjectRef`s not `np.ndarray`s# ray_map automatically converts np.sum to a `ray.remote` function which# automatically handles `ray.get`ing of `ray.ObjectRef`s passed as argumentsreturnray_map(self._ray_eval,values)defgen_starting_values(self):# generate objects and place in ray's object storereturn[ray.put(np.random.random(self.hparams.member_size)<0.5)foriinrange(self.hparams.population_size)]if__name__=='__main__':# initialize ray to use the specified system resourcesray.init()# create and train the populationmodule=OneMaxRayModule(population_size=128,member_size=1_000_000)pop,logbook,halloffame=Trainer(generations=200,progress=True).fit(module)print('initial stats:',logbook[0])print('final stats:',logbook[-1])print('best member:',halloffame.members[0])