Package: naturalselection
NaturalSelectionAn all-purpose pythonic genetic algorithm, which includes built-in hyperparameter tuning support for neural networks.Installation$ pip install naturalselectionUsageHere is a toy example optimising a pair of numbers with respect to division.>>>importnaturalselectionasns>>>>>>Pair=ns.Genus(x=range(1,10000),y=range(1,10000))>>>>>>pairs=ns.Population(...genus=Pair,...size=100,...fitness_fn=lambdan:n.x/n.y...)...>>>history=pairs.evolve(generations=100)Evolvingpopulation:100%|██████████████████|100/100[00:05<00:00,19.59it/s]>>>>>>history.fittest{'genome':{'x':9922,'y':10},'fitness':992.2}>>>>>>history.plot()We can also easily solve the classicalOneMax problem, which is about finding the bit-string of a given length with all 1's. Here we setgoal = 100in theevolvefunction to allow for early stopping if we reach our goal before the maximum number of generations, which we here set to 5,000. Note that it only takes nine seconds, however.>>>importnaturalselectionasns>>>>>>BitString=ns.Genus(**{f'x{n}':(0,1)forninrange(100)})>>>>>>defsum_bits(bitstring):...returnsum(bitstring.get_genome().values())...>>>bitstrings=ns.Population(...genus=BitString,...size=5,...fitness_fn=sum_bits...)...>>>history=bitstrings.evolve(generations=500,goal=100)Evolvingpopulation:36%|██████|1805/5000[00:09<00:16,194.43it/s]>>>>>>history.plot(only_show_max=True)Lastly, here is an example of finding a fully connected feedforward neural network to modelMNIST.Note that the models are trained in parallel, so it is loading in a copy of the MNIST data set for every CPU core in your computer, each of which takes up ~750MB of memory. If this causes you to run into memory trouble then you can set theworkersparameter to something small like 2, or setmultiprocessing = Falseto turn parallelism off completely. I've marked these in the code below.>>>importnaturalselectionasns>>>>>>defpreprocessing(X):...''' Basic normalisation and scaling preprocessing. '''...importnumpyasnp...X=X.reshape((-1,np.prod(X.shape[1:])))...X=(X-X.min())/(X.max()-X.min())...X-=X.mean(axis=0)...returnX...>>>defmnist_train_val_sets():...''' Get normalised and scaled MNIST train- and val sets. '''...fromtensorflow.keras.utilsimportto_categorical...importmnist...X_train=preprocessing(mnist.train_images())...Y_train=to_categorical(mnist.train_labels())...X_val=preprocessing(mnist.test_images())...Y_val=to_categorical(mnist.test_labels())...return(X_train,Y_train,X_val,Y_val)...>>>nns=ns.NNs(...size=30,...train_val_sets=mnist_train_val_sets(),...loss_fn='categorical_crossentropy',...score='accuracy',...output_activation='softmax',...max_epochs=1,...max_training_time=60,...# workers = 2, # If you want to reduce parallelism...# multiprocessing = False # If you want to disable parallelism...)...>>>history=nns.evolve(generations=20)Evolvingpopulation:100%|█████████████████████|20/20[57:18<00:00,73.22s/it]Computingfitness:100%|█████████████████████████|7/7[01:20<00:00,10.13s/it]>>>>>>history.fittest{'genome':{'optimizer':'adagrad','hidden_activation':'relu','batch_size':32,'initializer':'glorot_normal','input_dropout':0.2,'neurons0':256,'dropout0':0.0,'neurons1':128,'dropout1':0.1,'neurons2':256,'dropout2':0.1,'neurons3':256,'dropout3':0.2,'neurons4':128,'dropout4':0.4},'fitness':0.9659}>>>>>>history.plot(...title="Validation accuracy by generation",...ylabel="Validation accuracy"...)We can then train the best performing model and save it locally:>>># Training the best model and saving it to mnist_model.h5>>>best_score=nns.train_best(file_name='mnist_model')Epoch:0-loss:0.273,acc:0.924,val_loss:0.116,val_acc:0.966:100%|███████|60000/60000[00:12<00:00,1388.45it/s](...)Epoch:19-loss:0.029,acc:0.991,val_loss:0.073,val_acc:0.982:100%|██████|60000/60000[00:11<00:00,1846.24it/s]>>>>>>best_score0.982Algorithmic detailsThe algorithm follows the standard blueprint for a genetic algorithm as e.g. described on thisWikipedia page, which roughly goes like this:An initial population is constructedFitness values for all organisms in the population are computedA subset of the population (theelite pool) is selectedA subset of the population (thebreeding pool) is selectedPairs from the breeding pool are chosen, who will breed to create a new "child" organism with genome a combination of the "parent" organisms. Continue breeding until the children and the elites constitute a population of the same size as the originalA subset of the children (themutation pool) is selectedEvery child in the mutation pool is mutated, meaning that they will have their genome altered in some wayGo back to step 2We now describe the individual steps in this particular implementation in more detail. Note that step 3 is sometimes left out completely, but since that just corresponds to an empty elite pool I decided to keep it in, for generality.Step 1: Constructing the initial populationThe population is a uniformly random sample of the possible genome values as dictated by the genus, which is run when a newPopulationobject is created. Alternatively, you may set theinitial_genometo whatever genome you would like, which will create a population consisting of organisms similar to this genome (the result of starting with a population all equal to the organism and then mutating 80% of them).>>>pairs=ns.Population(...genus=Pair,...size=100,...fitness_fn=lambdan:n.x/n.y,...initial_genome={'x':9750,'y':15}...)...>>>history=pairs.evolve(generations=100)Evolvingpopulation:100%|██████████████████|100/100[00:05<00:00,19.47it/s]>>>>>>history.fittest{'genome':{'x':9989,'y':3},'fitness':3329.66666666665}Step 2: Compute fitness valuesThis happens in theupdate_fitnessfunction which is called by theevolvefunction. These computations will by default be computed in parallel when dealing with neural networks and serialised otherwise, as the benefits are only reaped when fitness computations take up a significant part of the algorithm (in the examples above not concerning neural networks we would actually slow down the algorithm non-trivially by introducing parallelism).Steps 3 & 4: Selecting elite pool and breeding poolThese two pools are selected in exactly the same way, using thesamplefunction. They only differ in the amount of organisms sampled, where the defaultelitism_rateis 5% andbreeding_rateis 80%. In the pool selection it chooses the population based on the distribution with density function the fitness value divided by the sum of all fitness values of the population. This means that the higher fitness score an organism has, the more likely it is for it to be chosen to be a part of the pool. The precise implementation of this is based on the algorithm specified on thisWikipedia page.Step 5: BreedingIn this implementation the parent organisms are chosen uniformly at random from the breeding pool. When determining the value of the child's genome we apply the "single-point crossover" method, where we choose an index uniformly at random among the attributes, and the child will then inherit all attributes to the left of this index from one parent and the attributes to the right of this index from the other parent.See more onthis Wikipedia page.Step 6: Selection of mutation poolThe mutation pool is chosen uniformly at random in contrast to the other two pools, as otherwise we would suddenly be more likely to "mutate away" many of the good genes of our fittest organisms. The defaultmutation_rateis 20%.Step 7: MutationThis implementation is roughly thebit string mutation, where every gene of the organism has a 1/n chance of being uniformly randomly replaced by another gene, with n being the number of genes in the organism's genome. This means that, on average, mutation causes one gene to be altered. The amount of genes altered in a mutation can be modified by changing thèmutation_factorparameter, which by default is the above 1/n.Possible future extensionsThese are the ideas that I have thought of implementing in the future. Check the ongoing process on thedevbranch.Enable support for CNNsEnable support for RNNs and in particular LSTMsInclude an option to have dependency relations between genes. In a neural network setting this could include the topology as a gene on which all the layer-specific genes depend upon, which would be similar to the approach taken inthis paper.LicenseThis project is licensed under theMIT License.