├── MANIFEST.in ├── __init__.py ├── .DS_Store ├── stochsearch ├── .DS_Store ├── __pycache__ │ ├── __init__.cpython-36.pyc │ └── evolutionary_search.cpython-36.pyc ├── __init__.py ├── README.md ├── microbial_search.py ├── evolutionary_search.py └── lamarckian_search.py ├── setup.py ├── LICENSE ├── demo ├── microbialsearch_demo.py └── evolsearch_demo.py ├── .gitignore └── .gitignore └── README.rst /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # for local testing 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madvn/stochsearch/HEAD/.DS_Store -------------------------------------------------------------------------------- /stochsearch/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madvn/stochsearch/HEAD/stochsearch/.DS_Store -------------------------------------------------------------------------------- /stochsearch/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madvn/stochsearch/HEAD/stochsearch/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /stochsearch/__pycache__/evolutionary_search.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madvn/stochsearch/HEAD/stochsearch/__pycache__/evolutionary_search.cpython-36.pyc -------------------------------------------------------------------------------- /stochsearch/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Packagifying and importing stochsearch classes 3 | 4 | Madhavun Candadai 5 | Jan 2018 6 | ''' 7 | __version__='5.0.5' 8 | from stochsearch.evolutionary_search import EvolSearch 9 | from stochsearch.microbial_search import MicrobialSearch 10 | from stochsearch.lamarckian_search import LamarckianSearch 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from stochsearch.__init__ import __version__ 3 | 4 | with open('README.rst') as f: 5 | readme = f.read() 6 | 7 | with open('LICENSE') as f: 8 | license = f.read() 9 | 10 | setup( 11 | name='stochsearch', 12 | version=__version__, 13 | description='A package that implements anumber of stochastic search algorithms using the pathos multiprocessing framework for parallelization', 14 | long_description=readme, 15 | author='Madhavun Candadai', 16 | author_email='madvncv@gmail.com', 17 | url='https://github.com/madvn/stochsearch', 18 | license=license, 19 | packages=['stochsearch'], 20 | install_requires=['numpy','pathos','dill','ppft'] 21 | ) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Madhavun Candadai Vasu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/microbialsearch_demo.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Demo of MicrobialSearch 3 | evolving for 10 dim vectors with each element in [0,1], maximizing their means 4 | i.e. best solution is [1,1,1,1,1,1,1,1,1,1] 5 | ''' 6 | 7 | import numpy as np 8 | from stochsearch import MicrobialSearch 9 | import matplotlib.pyplot as plt 10 | 11 | def fitness_function(individual): 12 | ''' 13 | sample fitness function 14 | ''' 15 | return np.mean(individual) 16 | 17 | # defining the parameters for the evolutionary search 18 | evol_params = { 19 | 'num_processes' : 12, # (optional) number of proccesses for multiprocessing.Pool 20 | 'pop_size' : 100, # population size 21 | 'genotype_size': 10, # dimensionality of solution 22 | 'fitness_function': fitness_function, # custom function defined to evaluate fitness of a solution 23 | 'recomb_prob': 0.1, # fraction of population retained as is between generations 24 | 'mutation_variance': 0.01, # mutation noise added to offspring. 25 | } 26 | 27 | # create evolutionary search object 28 | ms = MicrobialSearch(evol_params) 29 | 30 | '''OPTION 1''' 31 | # execute the search for 100 generations 32 | num_gens = 100 33 | hist = np.zeros((100,100)) 34 | best = [] 35 | avg = [] 36 | 37 | for i in range(num_gens): 38 | ms.step_generation() 39 | hist[i,:] = ms.get_fitnesses() 40 | best.append(ms.get_best_individual_fitness()) 41 | avg.append(ms.get_mean_fitness()) 42 | 43 | plt.pcolormesh(np.asarray(hist)) 44 | plt.xlabel('Fitness of each individual in population') 45 | plt.ylabel('Generations') 46 | plt.show() 47 | plt.plot(best,label='Best') 48 | plt.plot(avg,label='Average') 49 | plt.xlabel('Generations') 50 | plt.ylabel('Fitness') 51 | plt.show() 52 | -------------------------------------------------------------------------------- /.gitignore/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | *egg-info* 104 | .eggs/* 105 | __pycache__/* 106 | -------------------------------------------------------------------------------- /demo/evolsearch_demo.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Demo of EvolSearch 3 | evolving for 10 dim vectors with each element in [0,1], maximizing their means 4 | i.e. best solution is [1,1,1,1,1,1,1,1,1,1] 5 | ''' 6 | 7 | import numpy as np 8 | import matplotlib.pyplot as plt 9 | from stochsearch import EvolSearch 10 | 11 | def fitness_function(individual): 12 | ''' 13 | sample fitness function 14 | ''' 15 | return np.mean(individual) 16 | 17 | # defining the parameters for the evolutionary search 18 | evol_params = { 19 | 'num_processes' : 4, # (optional) number of proccesses for multiprocessing.Pool 20 | 'pop_size' : 100, # population size 21 | 'genotype_size': 50, # dimensionality of solution 22 | 'fitness_function': fitness_function, # custom function defined to evaluate fitness of a solution 23 | 'elitist_fraction': 0.04, # fraction of population retained as is between generations 24 | 'mutation_variance': 0.2 # mutation noise added to offspring. 25 | } 26 | 27 | # create evolutionary search object 28 | es = EvolSearch(evol_params) 29 | 30 | '''OPTION 1 31 | # execute the search for 100 generations 32 | num_gens = 100 33 | es.execute_search(num_gens) 34 | ''' 35 | 36 | '''OPTION 2''' 37 | # keep searching till a stopping condition is reached 38 | best_fit = [] 39 | mean_fit = [] 40 | num_gen = 0 41 | max_num_gens = 100 42 | desired_fitness = 0.98 43 | #while es.get_best_individual_fitness() < desired_fitness and num_gen < max_num_gens: 44 | while num_gen < max_num_gens: 45 | print('Gen #'+str(num_gen)+' Best Fitness = '+str(es.get_best_individual_fitness())) 46 | es.step_generation() 47 | best_fit.append(es.get_best_individual_fitness()) 48 | mean_fit.append(es.get_mean_fitness()) 49 | num_gen += 1 50 | 51 | # print results 52 | print('Max fitness of population = ',es.get_best_individual_fitness()) 53 | print('Best individual in population = ',es.get_best_individual()) 54 | 55 | # plot results 56 | plt.figure() 57 | plt.plot(best_fit) 58 | plt.plot(mean_fit) 59 | plt.xlabel('Generations') 60 | plt.ylabel('Fitness') 61 | plt.legend(['best fitness', 'avg. fitness']) 62 | plt.show() 63 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Stochastic Search 2 | ================= 3 | 4 | This Python stochastic search package, stochsearch, includes an 5 | implementation of algorithms such as evolutionary algorithm, microbial 6 | genetic algorithm, and lamarckian evolutionary algorithm, using the 7 | Python pathos multiprocessing framework. Fitness evaluation of 8 | individuals in a population is carried out in parallel across CPUs in a 9 | multiprocessing pool with the number of processes defined by the user or 10 | by os.cpu_count() of the system. Read below for installation and usage 11 | instructions. 12 | 13 | Installation 14 | ------------ 15 | 16 | :: 17 | 18 | $ pip install stochsearch 19 | 20 | Requirements: numpy, pathos 21 | 22 | Usage 23 | ----- 24 | 25 | This section illustrates how to use this package for evolutionary 26 | search. It is similar for other search methods. The only items that may 27 | change are the parameters of the search. See `this`_ for a description 28 | and list of parameters for each search method. 29 | 30 | Importing evolutionary search 31 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 32 | 33 | :: 34 | 35 | from stochsearch import EvolSearch 36 | 37 | Setup parameters for evolutionary search using a dictionary as follows 38 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 39 | 40 | :: 41 | 42 | evol_params = { 43 | 'num_processes' : 4, # (optional) number of proccesses for multiprocessing.Pool 44 | 'pop_size' : 100, # population size 45 | 'genotype_size': 10, # dimensionality of solution 46 | 'fitness_function': fitness_function, # custom function defined to evaluate fitness of a solution 47 | 'elitist_fraction': 0.04, # fraction of population retained as is between generations 48 | 'mutation_variance': 0.05, # mutation noise added to offspring. 49 | 'fitness_args': np.arange(100), # (optional) fitness_function *argv, len(list) should be 1 or pop_size 50 | } 51 | 52 | Define a function that takes a genotype as argument and returns the 53 | fitness value for that genotype - passed as the ‘fitness_function’ key 54 | in the evol_params dictionary. 55 | 56 | Create an evolutionary search object 57 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 58 | 59 | :: 60 | 61 | es = EvolSearch(evol_params) 62 | 63 | Executing the search 64 | ^^^^^^^^^^^^^^^^^^^^ 65 | 66 | Option 1: Run the search for a certain number of generations 67 | 68 | :: 69 | 70 | num_gens = 100 71 | es.execute_search(num_gens) 72 | 73 | Option 2: Step through the generations based on a condition 74 | 75 | :: 76 | 77 | max_num_gens = 100 78 | gen = 0 79 | desired_fitness = 0.9 80 | while es.get_best_individual_fitness() < desired_fitness and gen < max_num_gens: 81 | print("Gen #{} Best Fitness = {}".format(gen, es.get_best_individual_fitness())) 82 | es.step_generation() 83 | gen += 1 84 | 85 | Accessing results 86 | ^^^^^^^^^^^^^^^^^ 87 | 88 | :: 89 | 90 | print('Max fitness of population = ',es.get_best_individual_fitness()) 91 | print('Best individual in population = ',es.get_best_individual()) 92 | 93 | See `demos`_ folder for a sample script. 94 | 95 | .. _this: https://github.com/madvn/stochsearch/blob/master/stochsearch/README.md 96 | .. _demos: https://github.com/madvn/stochsearch/blob/master/demo/evolsearch_demo.py 97 | -------------------------------------------------------------------------------- /stochsearch/README.md: -------------------------------------------------------------------------------- 1 | ### Search algorithms and their parameters 2 | 3 | #### Evolutionary Search (from stochsearch import EvolSearch) 4 | An evolutionary algorithm is a stochastic search based optimization technique. It is a population based method, where optimization starts with a population of random solutions (individuals or genotypes). Each individual is assigned a fitness score based on how well they perform in the task at hand. Based on this fitness, a fraction of the best performing individuals are retained for the next iteration (generation). a new population of solutions is then created for the next generation with these 'elite' individuals and copies of them that have been subjected to mutation noise. This process is repeated either for a fixed number of generations, or until a desired fitness value is reached by the best individual in the population. In a non-stochastic system, this procedure will cause the fitness to be non-decreasing over generations. For those familiar with hill climbing, this approach can be seen as multiple hill climbers searching in parallel, where the number of hill climbers would be given by the elitist fraction of the population that are retained generation after generation. This same implementation can be used to perform hill-climbing if the elitist fraction is set such that elitist_fraction*population_size = 1. 5 | 6 | evol_params = { 7 | 'num_processes' : 4, # (optional) number of processes for multiprocessing.Pool 8 | 'pop_size' : 100, # population size 9 | 'genotype_size': 10, # dimensionality of solution 10 | 'fitness_function': fitness_function, # custom function defined to evaluate fitness of a solution 11 | 'elitist_fraction': 0.04, # fraction of population retained as is between generations 12 | 'mutation_variance': 0.05, # mutation noise added to offspring. 13 | 'fitness_args': np.arange(100), # (optional) fitness_function \*argv, len(list) should be 1 or pop_size 14 | } 15 | 16 | 17 | #### Microbial Search (from stochsearch import MicrobialSearch) 18 | This search method is quite similar to evolutionary except in its selection. This algorithm involves a tournament style selection. From the population list, in each generation, each individual competes against one of its neighbors based on a coin toss. The winner is put back in the population as is. The winner gets to "corrupt" the loser with its own genes based on a recombination probability (recomb_prob). The loser is then mutated based on a 0-mean gaussian noise with variance defined by mutation_variance and finally put back in its own position into the population. This process is repeated over several generations. 19 | 20 | evol_params = { 21 | 'num_processes' : 12, # (optional) number of processes for multiprocessing.Pool 22 | 'pop_size' : 100, # population size 23 | 'genotype_size': 10, # dimensionality of solution 24 | 'fitness_function': fitness_function, # custom function defined to evaluate fitness of a solution 25 | 'recomb_prob': 0.1, # probability of winner genes transfecting loser 26 | 'mutation_variance': 0.01, # mutation noise added to offspring. 27 | 'generations' : 100 28 | 'fitness_args': np.arange(100), # (optional) fitness_function \*argv, len(list) should be 1 or pop_size 29 | } 30 | 31 | #### Lamarckian evolution (from stochsearch import LamarckianSearch) 32 | This type of search involves some kind of learning during each generation. The fitness function that the user writes would receive a genotype, creates a phenotype (e.g. a neural network), train the phenotype (updated the weights of the neural network) and then evaluates its fitness. Once this is done, the fitness function returns the genotype remapped from the new trained phenotype and the fitness. The updated genotype is then put into the population. Once all individuals are evaluated like this, the population goes through the same elitist selection and mutation process as described in evolutionary search. 33 | 34 | evol_params = { 35 | 'num_processes' : 4, # (optional) number of processes for multiprocessing.Pool 36 | 'pop_size' : 100, # population size 37 | 'genotype_size': 10, # dimensionality of solution 38 | 'fitness_function': fitness_function, # custom function defined to evaluate fitness of a solution 39 | 'elitist_fraction': 0.04, # fraction of population retained as is between generations 40 | 'mutation_variance': 0.05, # mutation noise added to offspring. 41 | 'fitness_args': np.arange(100), # (optional) fitness_function \*argv, len(list) should be 1 or pop_size 42 | } 43 | -------------------------------------------------------------------------------- /stochsearch/microbial_search.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Multiprocessor Spatial Microbial GA 3 | 4 | Eduardo Izquierdo 5 | July, 2018 6 | ''' 7 | import random 8 | import time 9 | import numpy as np 10 | from pathos.multiprocessing import ProcessPool 11 | 12 | _pool = None 13 | 14 | class MicrobialSearch(): 15 | def __init__(self, evol_params): #generations, pop_size, genotype_size, recomb_prob, mutation_variance, num_processes): 16 | ''' 17 | Initialize evolutionary search 18 | ARGS: 19 | evol_params: dict 20 | required keys - 21 | pop_size: int - population size, 22 | genotype_size: int - genotype_size, 23 | fitness_function: function - a user-defined function that takes a genotype as arg and returns a float fitness value 24 | mutation_variance: float - variance of the gaussian distribution used for mutation noise 25 | recomb_prob: float between [0,1] -- proportion of genotype transfected from winner to loser 26 | num_processes: int - pool size for multiprocessing.pool.Pool - defaults to os.cpu_count() 27 | ''' 28 | # check for required keys 29 | required_keys = ['pop_size','genotype_size','fitness_function','recomb_prob','mutation_variance','num_processes'] 30 | for key in required_keys: 31 | if key not in evol_params.keys(): 32 | raise Exception('Argument evol_params does not contain the following required key: '+key) 33 | 34 | # checked for all required keys 35 | self.pop_size = evol_params['pop_size'] 36 | self.genotype_size = evol_params['genotype_size'] 37 | self.fitness_function = evol_params['fitness_function'] 38 | self.mutation_variance = evol_params['mutation_variance'] 39 | self.recomb_prob = evol_params['recomb_prob'] 40 | self.num_processes = evol_params['num_processes'] 41 | 42 | # validating fitness function 43 | assert self.fitness_function,"Invalid fitness_function" 44 | rand_genotype = np.random.rand(self.genotype_size) 45 | rand_genotype_fitness = self.fitness_function(rand_genotype) 46 | assert type(rand_genotype_fitness) == type(0.) or type(rand_genotype_fitness) in np.sctypes['float'],\ 47 | "Invalid return type for fitness_function. Should be float or np.dtype('np.float*')" 48 | 49 | # Search parameters 50 | self.group_size = int(self.pop_size/3) 51 | 52 | # Keep track of individuals to be mutated 53 | self.mutlist = np.zeros((self.group_size), dtype=int) 54 | self.mutfit = np.zeros((self.group_size)) 55 | 56 | # Creating the global process pool to be used across all generations 57 | global _pool 58 | _pool = ProcessPool(self.num_processes) 59 | time.sleep(0.5) 60 | 61 | # check for fitness function kwargs 62 | if 'fitness_args' in evol_params.keys(): 63 | optional_args = evol_params['fitness_args'] 64 | assert len(optional_args) == 1 or len(optional_args) == pop_size,\ 65 | "fitness args should be length 1 or pop_size." 66 | self.optional_args = optional_args 67 | else: 68 | self.optional_args = None 69 | 70 | # Create population and evaluate everyone once 71 | self.pop = np.random.random((self.pop_size,self.genotype_size)) 72 | self.fitness = np.asarray(_pool.map(self.evaluate_fitness,np.arange(self.pop_size))) 73 | 74 | def evaluate_fitness(self,individual_index): 75 | ''' 76 | Call user defined fitness function and pass genotype 77 | ''' 78 | if self.optional_args: 79 | if len(self.optional_args) == 1: 80 | return self.fitness_function(self.pop[individual_index,:], self.optional_args[0]) 81 | else: 82 | return self.fitness_function(self.pop[individual_index,:], self.optional_args[individual_index]) 83 | else: 84 | return self.fitness_function(self.pop[individual_index,:]) 85 | 86 | def step_generation(self): 87 | ''' 88 | evaluate fitness and step on generation 89 | ''' 90 | global _pool 91 | # Perform tournament for every individual in population 92 | for j in range(3): 93 | k = 0 94 | for a in range(j,self.pop_size-2,3): 95 | # Step 1: Pick 2nd individual as left or right hand side neighbor of first 96 | b = (a+random.choice([-1,1]))%self.pop_size 97 | # Step 2: Compare their fitness 98 | if (self.fitness[a] > self.fitness[b]): 99 | winner = a 100 | loser = b 101 | else: 102 | winner = b 103 | loser = a 104 | # Step 3: Transfect loser with winner 105 | for l in range(self.genotype_size): 106 | if (random.random() < self.recomb_prob): 107 | self.pop[loser][l] = self.pop[winner][l] 108 | # Step 4: Mutate loser 109 | m = np.random.normal(0.0, self.mutation_variance, self.genotype_size) 110 | self.pop[loser] = np.clip(np.add(self.pop[loser],m),0.0,1.0) 111 | # Step 5: Add to mutated list (which will be re-evaluated) 112 | self.mutlist[k]=loser 113 | k+=1 114 | # Step 6: Recalculate fitness of list of mutated losers 115 | self.mutfit = list(_pool.map(self.evaluate_fitness, self.mutlist)) 116 | for k in range(self.group_size): 117 | self.fitness[self.mutlist[k]] = self.mutfit[k] 118 | 119 | def execute_search(self, num_gens): 120 | ''' 121 | runs the evolutionary algorithm for given number of generations, num_gens 122 | ''' 123 | for _ in range(num_gens): 124 | self.step_generation() 125 | 126 | def get_fitnesses(self): 127 | ''' 128 | simply return all fitness values of current population 129 | ''' 130 | return self.fitness 131 | 132 | def get_best_individual(self): 133 | ''' 134 | returns 1D array of the genotype that has max fitness 135 | ''' 136 | return self.pop[np.argmax(self.fitness),:] 137 | 138 | def get_best_individual_fitness(self): 139 | ''' 140 | return the fitness value of the best individual 141 | ''' 142 | return np.max(self.fitness) 143 | 144 | def get_mean_fitness(self): 145 | ''' 146 | returns the mean fitness of the population 147 | ''' 148 | return np.mean(self.fitness) 149 | 150 | def get_fitness_variance(self): 151 | ''' 152 | returns variance of the population's fitness 153 | ''' 154 | return np.std(self.fitness)**2 155 | -------------------------------------------------------------------------------- /stochsearch/evolutionary_search.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Contains the multiprocessing evolutionary search class 3 | 4 | Madhavun Candadai 5 | Jan, 2018 6 | ''' 7 | #from multiprocessing import Pool 8 | import time 9 | import numpy as np 10 | from pathos.multiprocessing import ProcessPool 11 | 12 | __evolsearch_process_pool = None 13 | 14 | class EvolSearch: 15 | def __init__(self,evol_params): 16 | ''' 17 | Initialize evolutionary search 18 | ARGS: 19 | evol_params: dict 20 | required keys - 21 | pop_size: int - population size, 22 | genotype_size: int - genotype_size, 23 | fitness_function: function - a user-defined function that takes a genotype as arg and returns a float fitness value 24 | elitist_fraction: float - fraction of top performing individuals to retain for next generation 25 | mutation_variance: float - variance of the gaussian distribution used for mutation noise 26 | optional keys - 27 | fitness_args: list-like - optional additional arguments to pass while calling fitness function 28 | list such that len(list) == 1 or len(list) == pop_size 29 | num_processes: int - pool size for multiprocessing.pool.Pool - defaults to os.cpu_count() 30 | ''' 31 | # check for required keys 32 | required_keys = ['pop_size','genotype_size','fitness_function','elitist_fraction','mutation_variance'] 33 | for key in required_keys: 34 | if key not in evol_params.keys(): 35 | raise Exception('Argument evol_params does not contain the following required key: '+key) 36 | 37 | # checked for all required keys 38 | self.pop_size = evol_params['pop_size'] 39 | self.genotype_size = evol_params['genotype_size'] 40 | self.fitness_function = evol_params['fitness_function'] 41 | self.elitist_fraction = int(np.ceil(evol_params['elitist_fraction']*self.pop_size)) 42 | self.mutation_variance = evol_params['mutation_variance'] 43 | 44 | # validating fitness function 45 | assert self.fitness_function,"Invalid fitness_function" 46 | rand_genotype = np.random.rand(self.genotype_size) 47 | rand_genotype_fitness = self.fitness_function(rand_genotype) 48 | assert type(rand_genotype_fitness) == type(0.) or type(rand_genotype_fitness) in np.sctypes['float'],\ 49 | "Invalid return type for fitness_function. Should be float or np.dtype('np.float*')" 50 | 51 | # create other required data 52 | self.num_processes = evol_params.get('num_processes',None) 53 | self.pop = np.random.rand(self.pop_size,self.genotype_size) 54 | self.fitness = np.zeros(self.pop_size) 55 | self.num_batches = int(self.pop_size/self.num_processes) 56 | self.num_remainder = int(self.pop_size%self.num_processes) 57 | 58 | # check for fitness function kwargs 59 | if 'fitness_args' in evol_params.keys(): 60 | optional_args = evol_params['fitness_args'] 61 | assert len(optional_args) == 1 or len(optional_args) == self.pop_size,\ 62 | "fitness args should be length 1 or pop_size." 63 | self.optional_args = optional_args 64 | else: 65 | self.optional_args = None 66 | 67 | # creating the global process pool to be used across all generations 68 | global __evolsearch_process_pool 69 | __evolsearch_process_pool = ProcessPool(self.num_processes) 70 | time.sleep(0.5) 71 | 72 | def evaluate_fitness(self,individual_index): 73 | ''' 74 | Call user defined fitness function and pass genotype 75 | ''' 76 | if self.optional_args: 77 | if len(self.optional_args) == 1: 78 | return self.fitness_function(self.pop[individual_index,:], self.optional_args[0]) 79 | else: 80 | return self.fitness_function(self.pop[individual_index,:], self.optional_args[individual_index]) 81 | else: 82 | return self.fitness_function(self.pop[individual_index,:]) 83 | 84 | def elitist_selection(self): 85 | ''' 86 | from fitness select top performing individuals based on elitist_fraction 87 | ''' 88 | self.pop = self.pop[np.argsort(self.fitness)[-self.elitist_fraction:],:] 89 | 90 | def mutation(self): 91 | ''' 92 | create new pop by repeating mutated copies of elitist individuals 93 | ''' 94 | # number of copies of elitists required 95 | num_reps = int((self.pop_size-self.elitist_fraction)/self.elitist_fraction)+1 96 | 97 | # creating copies and adding noise 98 | mutated_elites = np.tile(self.pop,[num_reps,1]) 99 | mutated_elites += np.random.normal(loc=0.,scale=self.mutation_variance, 100 | size=[num_reps*self.elitist_fraction,self.genotype_size]) 101 | 102 | # concatenating elites with their mutated versions 103 | self.pop = np.vstack((self.pop,mutated_elites)) 104 | 105 | # clipping to pop_size 106 | self.pop = self.pop[:self.pop_size,:] 107 | 108 | # clipping to genotype range 109 | self.pop = np.clip(self.pop,0,1) 110 | 111 | def step_generation(self): 112 | ''' 113 | evaluate fitness of pop, and create new pop after elitist_selection and mutation 114 | ''' 115 | global __evolsearch_process_pool 116 | 117 | # estimate fitness using multiprocessing pool 118 | if __evolsearch_process_pool: 119 | # pool exists 120 | self.fitness = np.asarray(__evolsearch_process_pool.map(self.evaluate_fitness,np.arange(self.pop_size))) 121 | else: 122 | # re-create pool 123 | __evolsearch_process_pool = Pool(self.num_processes) 124 | self.fitness = np.asarray(__evolsearch_process_pool.map(self.evaluate_fitness,np.arange(self.pop_size))) 125 | 126 | # elitist_selection 127 | self.elitist_selection() 128 | 129 | # mutation 130 | self.mutation() 131 | 132 | def execute_search(self,num_gens): 133 | ''' 134 | runs the evolutionary algorithm for given number of generations, num_gens 135 | ''' 136 | # step generation num_gens times 137 | for gen in np.arange(num_gens): 138 | self.step_generation() 139 | 140 | def get_fitnesses(self): 141 | ''' 142 | simply return all fitness values of current population 143 | ''' 144 | return self.fitness 145 | 146 | def get_best_individual(self): 147 | ''' 148 | returns 1D array of the genotype that has max fitness 149 | ''' 150 | return self.pop[np.argmax(self.fitness),:] 151 | 152 | def get_best_individual_fitness(self): 153 | ''' 154 | return the fitness value of the best individual 155 | ''' 156 | return np.max(self.fitness) 157 | 158 | def get_mean_fitness(self): 159 | ''' 160 | returns the mean fitness of the population 161 | ''' 162 | return np.mean(self.fitness) 163 | 164 | def get_fitness_variance(self): 165 | ''' 166 | returns variance of the population's fitness 167 | ''' 168 | return np.std(self.fitness)**2 169 | 170 | if __name__ == "__main__": 171 | def fitness_function(individual): 172 | ''' 173 | sample fitness function 174 | ''' 175 | return np.mean(individual) 176 | 177 | # defining the parameters for the evolutionary search 178 | evol_params = { 179 | 'num_processes' : 4, # (optional) number of proccesses for multiprocessing.Pool 180 | 'pop_size' : 100, # population size 181 | 'genotype_size': 10, # dimensionality of solution 182 | 'fitness_function': fitness_function, # custom function defined to evaluate fitness of a solution 183 | 'elitist_fraction': 0.04, # fraction of population retained as is between generations 184 | 'mutation_variance': 0.05 # mutation noise added to offspring. 185 | } 186 | 187 | # create evolutionary search object 188 | es = EvolSearch(evol_params) 189 | 190 | '''OPTION 1 191 | # execute the search for 100 generations 192 | num_gens = 100 193 | es.execute_search(num_gens) 194 | ''' 195 | 196 | '''OPTION 2''' 197 | # keep searching till a stopping condition is reached 198 | num_gen = 0 199 | max_num_gens = 100 200 | desired_fitness = 0.75 201 | while es.get_best_individual_fitness() < desired_fitness and num_gen < max_num_gens: 202 | print('Gen #'+str(num_gen)+' Best Fitness = '+str(es.get_best_individual_fitness())) 203 | es.step_generation() 204 | num_gen += 1 205 | 206 | # print results 207 | print('Max fitness of population = ',es.get_best_individual_fitness()) 208 | print('Best individual in population = ',es.get_best_individual()) 209 | -------------------------------------------------------------------------------- /stochsearch/lamarckian_search.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Contains the multiprocessing Lamarckian search class 3 | 4 | Madhavun Candadai 5 | Sep, 2018 6 | ''' 7 | #from multiprocessing import Pool 8 | import time 9 | import numpy as np 10 | from pathos.multiprocessing import ProcessPool 11 | 12 | __search_process_pool = None 13 | 14 | class LamarckianSearch: 15 | def __init__(self,evol_params): 16 | ''' 17 | Initialize evolutionary search 18 | ARGS: 19 | evol_params: dict 20 | required keys - 21 | pop_size: int - population size, 22 | genotype_size: int - genotype_size, 23 | fitness_function: function - a user-defined function that takes a genotype as arg and returns updated genotype and float fitness value 24 | elitist_fraction: float - fraction of top performing individuals to retain for next generation 25 | mutation_variance: float - variance of the gaussian distribution used for mutation noise 26 | optional keys - 27 | fitness_args: list-like - optional additional arguments to pass while calling fitness function 28 | list such that len(list) == 1 or len(list) == pop_size 29 | num_processes: int - pool size for multiprocessing.pool.Pool - defaults to os.cpu_count() 30 | ''' 31 | # check for required keys 32 | required_keys = ['pop_size','genotype_size','fitness_function','elitist_fraction','mutation_variance'] 33 | for key in required_keys: 34 | if key not in evol_params.keys(): 35 | raise Exception('Argument evol_params does not contain the following required key: {}'.format(key)) 36 | 37 | # checked for all required keys 38 | self.pop_size = evol_params['pop_size'] 39 | self.genotype_size = evol_params['genotype_size'] 40 | self.fitness_function = evol_params['fitness_function'] 41 | self.elitist_fraction = int(np.ceil(evol_params['elitist_fraction']*self.pop_size)) 42 | self.mutation_variance = evol_params['mutation_variance'] 43 | 44 | # validating fitness function 45 | assert self.fitness_function,"Invalid fitness_function" 46 | rand_genotype = np.random.rand(self.genotype_size) 47 | fitness_return = self.fitness_function(rand_genotype) 48 | assert len(fitness_return) == 2, "Fitness function must return 2 items - updated_genotype and fitness" 49 | updated_genotype = fitness_return[0] 50 | rand_genotype_fitness = fitness_return[1] 51 | assert type(rand_genotype_fitness) == type(0.) or type(rand_genotype_fitness) in np.sctypes['float'],\ 52 | "Invalid return type for second return of fitness_function. Should be float or np.dtype('np.float*')" 53 | assert len(updated_genotype) == self.genotype_size, \ 54 | "Invalid length for first return type of fitness function: length should be equal to genotype_size={}".format(self.genotype_size) 55 | 56 | # create other required data 57 | self.num_processes = evol_params.get('num_processes',None) 58 | self.pop = np.random.rand(self.pop_size,self.genotype_size) 59 | self.fitness = np.zeros(self.pop_size) 60 | self.num_batches = int(self.pop_size/self.num_processes) 61 | self.num_remainder = int(self.pop_size%self.num_processes) 62 | 63 | # check for fitness function kwargs 64 | if 'fitness_args' in evol_params.keys(): 65 | optional_args = evol_params['fitness_args'] 66 | assert len(optional_args) == 1 or len(optional_args) == self.pop_size,\ 67 | "fitness args should be length 1 or pop_size." 68 | self.optional_args = optional_args 69 | else: 70 | self.optional_args = None 71 | 72 | # creating the global process pool to be used across all generations 73 | global __search_process_pool 74 | __search_process_pool = ProcessPool(self.num_processes) 75 | time.sleep(0.5) 76 | 77 | def evaluate_fitness(self,individual_index): 78 | ''' 79 | Call user defined fitness function and pass genotype 80 | ''' 81 | if self.optional_args: 82 | if len(self.optional_args) == 1: 83 | individual, fitness = self.fitness_function(self.pop[individual_index,:], self.optional_args[0]) 84 | else: 85 | individual, fitness = self.fitness_function(self.pop[individual_index,:], self.optional_args[individual_index]) 86 | else: 87 | individual, fitness = self.fitness_function(self.pop[individual_index,:]) 88 | 89 | # inserting updated genotype back into population 90 | self.pop[individual_index] = individual 91 | return fitness 92 | 93 | def elitist_selection(self): 94 | ''' 95 | from fitness select top performing individuals based on elitist_fraction 96 | ''' 97 | self.pop = self.pop[np.argsort(self.fitness)[-self.elitist_fraction:],:] 98 | 99 | def mutation(self): 100 | ''' 101 | create new pop by repeating mutated copies of elitist individuals 102 | ''' 103 | # number of copies of elitists required 104 | num_reps = int((self.pop_size-self.elitist_fraction)/self.elitist_fraction)+1 105 | 106 | # creating copies and adding noise 107 | mutated_elites = np.tile(self.pop,[num_reps,1]) 108 | mutated_elites += np.random.normal(loc=0.,scale=self.mutation_variance, 109 | size=[num_reps*self.elitist_fraction,self.genotype_size]) 110 | 111 | # concatenating elites with their mutated versions 112 | self.pop = np.vstack((self.pop,mutated_elites)) 113 | 114 | # clipping to pop_size 115 | self.pop = self.pop[:self.pop_size,:] 116 | 117 | # clipping to genotype range 118 | self.pop = np.clip(self.pop,0,1) 119 | 120 | def step_generation(self): 121 | ''' 122 | evaluate fitness of pop, and create new pop after elitist_selection and mutation 123 | ''' 124 | global __search_process_pool 125 | 126 | # estimate fitness using multiprocessing pool 127 | if __search_process_pool: 128 | # pool exists 129 | self.fitness = np.asarray(__search_process_pool.map(self.evaluate_fitness,np.arange(self.pop_size))) 130 | else: 131 | # re-create pool 132 | __search_process_pool = Pool(self.num_processes) 133 | self.fitness = np.asarray(__search_process_pool.map(self.evaluate_fitness,np.arange(self.pop_size))) 134 | 135 | # elitist_selection 136 | self.elitist_selection() 137 | 138 | # mutation 139 | self.mutation() 140 | 141 | def execute_search(self,num_gens): 142 | ''' 143 | runs the evolutionary algorithm for given number of generations, num_gens 144 | ''' 145 | # step generation num_gens times 146 | for gen in np.arange(num_gens): 147 | self.step_generation() 148 | 149 | def get_fitnesses(self): 150 | ''' 151 | simply return all fitness values of current population 152 | ''' 153 | return self.fitness 154 | 155 | def get_best_individual(self): 156 | ''' 157 | returns 1D array of the genotype that has max fitness 158 | ''' 159 | return self.pop[np.argmax(self.fitness),:] 160 | 161 | def get_best_individual_fitness(self): 162 | ''' 163 | return the fitness value of the best individual 164 | ''' 165 | return np.max(self.fitness) 166 | 167 | def get_mean_fitness(self): 168 | ''' 169 | returns the mean fitness of the population 170 | ''' 171 | return np.mean(self.fitness) 172 | 173 | def get_fitness_variance(self): 174 | ''' 175 | returns variance of the population's fitness 176 | ''' 177 | return np.std(self.fitness)**2 178 | 179 | if __name__ == "__main__": 180 | def fitness_function(individual): 181 | ''' 182 | sample fitness function 183 | ''' 184 | return individual, np.mean(individual) 185 | 186 | # defining the parameters for the evolutionary search 187 | evol_params = { 188 | 'num_processes' : 4, # (optional) number of proccesses for multiprocessing.Pool 189 | 'pop_size' : 100, # population size 190 | 'genotype_size': 10, # dimensionality of solution 191 | 'fitness_function': fitness_function, # custom function defined to evaluate fitness of a solution 192 | 'elitist_fraction': 0.04, # fraction of population retained as is between generations 193 | 'mutation_variance': 0.05 # mutation noise added to offspring. 194 | } 195 | 196 | # create evolutionary search object 197 | es = LamarckianSearch(evol_params) 198 | 199 | '''OPTION 1 200 | # execute the search for 100 generations 201 | num_gens = 100 202 | es.execute_search(num_gens) 203 | ''' 204 | 205 | '''OPTION 2''' 206 | # keep searching till a stopping condition is reached 207 | num_gen = 0 208 | max_num_gens = 100 209 | desired_fitness = 0.75 210 | while es.get_best_individual_fitness() < desired_fitness and num_gen < max_num_gens: 211 | print('Gen #'+str(num_gen)+' Best Fitness = '+str(es.get_best_individual_fitness())) 212 | es.step_generation() 213 | num_gen += 1 214 | 215 | # print results 216 | print('Max fitness of population = ',es.get_best_individual_fitness()) 217 | print('Best individual in population = ',es.get_best_individual()) 218 | --------------------------------------------------------------------------------