Skip to content

Binder

Evolutionary parameter search with a single neural mass model

This notebook provides a simple example for the use of the evolutionary optimization framework built-in to the library. Under the hood, the implementation of the evolutionary algorithm is powered by deap and pypet cares about the parallelization and storage of the simulation data for us.

We want to optimize for a simple target, namely finding a parameter configuration that produces activity with a peak power frequency spectrum at 25 Hz.

In this notebook, we will also plot the evolutionary genealogy tree, to visualize how the population evolves over generations.

# change to the root directory of the project
import os
if os.getcwd().split("/")[-1] == "examples":
    os.chdir('..')

# This will reload all imports as soon as the code changes
%load_ext autoreload
%autoreload 2
try:
    import matplotlib.pyplot as plt
except ImportError:
    import sys
    !{sys.executable} -m pip install matplotlib seaborn
    import matplotlib.pyplot as plt

import numpy as np
import logging 

from neurolib.models.aln import ALNModel
from neurolib.utils.parameterSpace import ParameterSpace
from neurolib.optimize.evolution import Evolution
import neurolib.utils.functions as func

import neurolib.optimize.evolution.deapUtils as deapUtils

# a nice color map
plt.rcParams['image.cmap'] = 'plasma'

Model definition

aln = ALNModel()
# Here we define our evaluation function. This function will
# be called reapedly and perform a single simulation. The object
# that is passed to the function, `traj`, is a pypet trajectory
# and serves as a "bridge" to load the parameter set of this 
# particular trajectory and execute a run.
# Then the power spectrum of the run is computed and its maximum
# is fitted to the target of 25 Hz peak frequency.
def evaluateSimulation(traj):
    # The trajectory id is provided as an attribute
    rid = traj.id
    logging.info("Running run id {}".format(rid))
    # this function provides the a model with the partuclar
    # parameter set for this given run
    model = evolution.getModelFromTraj(traj)
    # parameters can also be modified after loading
    model.params['dt'] = 0.1
    model.params['duration'] = 2*1000.
    # and the simulation is run
    model.run()

    # compute power spectrum
    frs, powers = func.getPowerSpectrum(model.rates_exc[:, -int(1000/model.params['dt']):], dt=model.params['dt'])
    # find the peak frequency
    domfr = frs[np.argmax(powers)] 
    # fitness evaluation: let's try to find a 25 Hz oscillation
    fitness = abs(domfr - 25) 
    # deap needs a fitness *tuple*!
    fitness_tuple = ()
    # more fitness values could be added
    fitness_tuple += (fitness, )
    # we need to return the fitness tuple and the outputs of the model
    return fitness_tuple, model.outputs

Initialize and run evolution

The evolutionary algorithm tries to find the optimal parameter set that will maximize (or minimize) a certain fitness function.

This achieved by seeding an initial population of size POP_INIT_SIZE that is randomly initiated in the parameter space parameterSpace. INIT: After simulating the initial population using evalFunction, only a subset of the individuals is kept, defined by POP_SIZE.

START: Members of the remaining population are chosen based on their fitness (using rank selection) to mate and produce offspring. These offspring have parameters that are drawn from a normal distribution defined by the mean of the parameters between the two parents. Then the offspring population is evaluated and the process loops back to START:

This process is repeated for NGEN generations.

# Here we define the parameters and the range in which we want
# to perform the evolutionary optimization.
# Create a `ParameterSpace` 
pars = ParameterSpace(['mue_ext_mean', 'mui_ext_mean'], [[0.0, 4.0], [0.0, 4.0]])
# Iitialize evolution with
# :evaluateSimulation: The function that returns a fitness, 
# :pars: The parameter space and its boundaries to optimize
# :model: The model that should be passed to the evaluation function
# :weightList: A list of optimization weights for the `fitness_tuple`,
#              positive values will lead to a maximization, negative 
#              values to a minimzation. The length of this list must
#              be the same as the length of the `fitness_tuple`.
# 
# :POP_INIT_SIZE: The size of the initial population that will be 
#              randomly sampled in the parameter space `pars`.
#              Should be higher than POP_SIZE. 50-200 might be a good
#              range to start experimenting with.
# :POP_SIZE: Size of the population that should evolve. Must be an
#              even number. 20-100 might be a good range to start with.
# :NGEN: Number of generations to simulate the evolution for. A good
#              range to start with might be 20-100.

weightList = [-1.0]

evolution = Evolution(evalFunction = evaluateSimulation, parameterSpace = pars, model = aln, weightList = [-1.0],
                      POP_INIT_SIZE=4, POP_SIZE = 4, NGEN=2, filename="example-2.1.hdf")
# info: chose POP_INIT_SIZE=50, POP_SIZE = 20, NGEN=20 for real exploration, 
# values are lower here for testing
# Enabling `verbose = True` will print statistics and generate plots 
# of the current population for each generation.
evolution.run(verbose = False)

Analysis

Population

# the current population is always accesible via
pop = evolution.pop
# we can also use the functions registered to deap
# to select the best of the population:
best_10 = evolution.toolbox.selBest(pop, k=10)
# Remember, we performed a minimization so a fitness
# of 0 is optimal
print("Best individual", best_10[0], "fitness", best_10[0].fitness)
Best individual [1.182184510022096, 0.29660620374273683, 0.4936712969767474, 0.07875430013351538] fitness (0.0,)

We can look at the current population by calling evolution.dfPop() which returns a pandas dataframe with the parameters of each individual, its id, generation of birth, its outputs, and the fitness (called "f0" here).

evolution.dfPop(outputs=True)
mue_ext_mean mui_ext_mean score id gen t rates_exc rates_inh IA f0
0 1.182185 0.296606 0.0 294 13 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 0.0
1 1.114270 0.240422 0.0 368 16 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 0.0
2 0.910558 0.075463 0.0 403 18 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 0.0
3 1.188440 0.356385 -1.0 171 7 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 1.0
4 1.007371 0.113623 -1.0 177 7 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 1.0
5 1.031484 0.120989 -1.0 192 8 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 1.0
6 0.900787 0.038763 -1.0 193 8 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 1.0
7 1.217021 0.213936 -1.0 245 10 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 1.0
8 1.241895 0.365758 -1.0 248 10 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 1.0
9 1.062928 0.265389 -1.0 267 11 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 1.0
10 1.007366 0.110587 -1.0 286 12 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 1.0
11 0.904612 0.123308 -1.0 320 14 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 1.0
12 1.119281 0.188307 -1.0 330 15 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 1.0
13 1.158463 0.227194 -1.0 342 15 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 1.0
14 1.053327 0.281852 -1.0 344 15 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 1.0
15 1.124747 0.318747 -1.0 360 16 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 1.0
16 1.266317 0.360644 -1.0 364 16 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 1.0
17 1.329988 0.388133 -1.0 365 16 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 1.0
18 0.986030 0.189384 -1.0 390 18 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 1.0
19 0.896915 0.125212 -1.0 399 18 [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.60... [[4.3201372225314945, 3.9353836030865286, 3.58... [[10.920182546008649, 11.229353479381396, 11.5... [[111.41612911461853, 111.36042105006122, 111.... 1.0

You can also view all individuals that were created during the entire evolution, by calling evolution.dfEvolution():

evolution.dfEvolution()
mue_ext_mean mui_ext_mean score id gen f0
0 1.400310 1.209331 -4.0 39 0 4.0
1 1.173593 0.662050 -5.0 31 0 5.0
2 1.134601 0.809371 -6.0 22 0 6.0
3 0.992049 0.694590 -6.0 29 0 6.0
4 1.470708 1.073607 -7.0 47 0 7.0
... ... ... ... ... ... ...
395 1.881591 0.299691 -24.0 425 19 24.0
396 0.681422 0.489003 -8.0 426 19 8.0
397 1.430791 0.268028 -24.0 427 19 24.0
398 1.275903 0.534227 -3.0 428 19 3.0
399 0.870652 0.326687 -5.0 429 19 5.0

400 rows × 6 columns

# a sinple overview of the current population (in this case the 
# last one) is given via the `info()` method. This provides a 
# a histogram of the score (= mean fitness) and scatterplots
# and density estimates across orthogonal parameter space cross 
# sections.
evolution.info(plot=True)
> Simulation parameters
HDF file storage: ./data/hdf/example-2.1.hdf
Trajectory Name: results-2020-07-02-14H-20M-45S
Duration of evaluating initial population 0:00:29.656935
Duration of evolution 0:03:50.565418
Model: <class 'neurolib.models.aln.model.ALNModel'>
Model name: aln
Eval function: <function evaluateSimulation at 0x10ba8cae8>
Parameter space: {'mue_ext_mean': [0.0, 4.0], 'mui_ext_mean': [0.0, 4.0]}
> Evolution parameters
Number of generations: 20
Initial population size: 50
Population size: 20
> Evolutionary operators
Mating operator: <function cxBlend at 0x11dcaf510>
Mating paramter: {'alpha': 0.5}
Selection operator: <function selBest_multiObj at 0x11f4d9d08>
Selection paramter: {}
Parent selection operator: <function selRank at 0x11f4d9c80>
Comments: no comments
--- Info summary ---
Valid: 20
Mean score (weighted fitness): -0.85
Parameter distribution (Generation 19):
mue_ext_mean:    mean: 1.0852,   std: 0.1270
mui_ext_mean:    mean: 0.2200,   std: 0.1042
--------------------
Best 5 individuals:
Printing 5 individuals
Individual 0
    Fitness values:  0.0
    Score:  0.0
    Weighted fitness:  -0.0
    Stats mean 0.00 std 0.00 min 0.00 max 0.00
    model.params["mue_ext_mean"] = 1.18
    model.params["mui_ext_mean"] = 0.30
Individual 1
    Fitness values:  0.0
    Score:  0.0
    Weighted fitness:  -0.0
    Stats mean 0.00 std 0.00 min 0.00 max 0.00
    model.params["mue_ext_mean"] = 1.11
    model.params["mui_ext_mean"] = 0.24
Individual 2
    Fitness values:  0.0
    Score:  0.0
    Weighted fitness:  -0.0
    Stats mean 0.00 std 0.00 min 0.00 max 0.00
    model.params["mue_ext_mean"] = 0.91
    model.params["mui_ext_mean"] = 0.08
Individual 3
    Fitness values:  1.0
    Score:  -1.0
    Weighted fitness:  -1.0
    Stats mean 1.00 std 0.00 min 1.00 max 1.00
    model.params["mue_ext_mean"] = 1.19
    model.params["mui_ext_mean"] = 0.36
Individual 4
    Fitness values:  1.0
    Score:  -1.0
    Weighted fitness:  -1.0
    Stats mean 1.00 std 0.00 min 1.00 max 1.00
    model.params["mue_ext_mean"] = 1.01
    model.params["mui_ext_mean"] = 0.11
--------------------

MainProcess root INFO     Saving plot to ./data/figures/results-2020-07-02-14H-20M-45S_hist_19.png

There are 20 valid individuals
Mean score across population: -0.85

<Figure size 432x288 with 0 Axes>

Plotting genealogy tree

neurolib keeps track of all individuals during the evolution. You can see all individuals from each generation by calling evolution.history. The object evolution.tree provides a network description of the genealogy of the evolution: each individual (indexed by its unique .id) is connected to its parents. We can use this object in combination with the network library networkx to plot the tree:

# we put this into a try except block since we don't do testing on networkx
try:
    import matplotlib.pyplot as plt
    import networkx as nx
    from networkx.drawing.nx_pydot import graphviz_layout

    G = nx.DiGraph(evolution.tree)
    G = G.reverse()     # Make the graph top-down
    pos = graphviz_layout(G, prog='dot')
    plt.figure(figsize=(8, 8))
    nx.draw(G, pos, node_size=50, alpha=0.5, node_color=list(evolution.id_score.values()), with_labels=False)
    plt.show()
except:
    print("It looks like networkx or pydot are not installed")
/Users/caglar/anaconda/lib/python3.7/site-packages/networkx/drawing/nx_pylab.py:579: MatplotlibDeprecationWarning: 
The iterable function was deprecated in Matplotlib 3.1 and will be removed in 3.3. Use np.iterable instead.
  if not cb.iterable(width):
/Users/caglar/anaconda/lib/python3.7/site-packages/networkx/drawing/nx_pylab.py:676: MatplotlibDeprecationWarning: 
The iterable function was deprecated in Matplotlib 3.1 and will be removed in 3.3. Use np.iterable instead.
  if cb.iterable(node_size):  # many node sizes