Skeleton Parameter Search

To demonstrate the use of PGENESIS for doing parameter searching, we have constructed an example that performs a simple genetic search.

In performing the genetic search, we keep a fixed-sized population of individuals "alive". We randomly pick an individual from this population and mutate its representation with a certain probability. We then evaluate the new individual and, if it is better than some other, we replace the worst-evaluated individual in the population with this new individual. To allow for parallelism, we evaluate multiple new individuals simultaneously, and only remove existing individuals in the population as new individuals with superior evaluations are found.

In this example we represent each parameter (out of a total of 4 parameters per individual) as a 16-bit string. The floating point value which this bit-string represents is determined by mapping a 16 bit integer (with Gray code representation) with range [0,65535] into the range [-32.768, 32.767]. When constructing a new individual from an old one, we mutate each of the bits with probability 0.02. We evaluate each individual by computing a trivial function over the parameters. In a real-world parameter search, this step would be the most time-consuming since it would involve running a neural simulation. Here, we have set our evaluation function to 1.0 / sqrt((a - 3)^2 + (b - 5)^2 + (c - 7)^2 + (d - 11)^2). This directs the search toward values for parameters a, b, c, and d that equal 3, 5, 7, and 11, respectively.

The main PGENESIS script follows. Note that because the individuals can run independently of each other, here PGENESIS is started in "farm" mode i.e., with each node running in its own zone. Also note that synchronizing zones is accomplished with the barrierall rather than barrier command.

    // every node will run these script commands
    paron -farm -silent 0 -nodes {n_nodes} -output o.out \
           -altsimrc ../.simrc -executable nxpgenesis
    echo I am node {mytotalnode}

    // first wait for every node to start up
    barrierall

    if ({mytotalnode} == 0)
           search          // the master (on node 0) runs the parameter search
    end
    barrierall 7 1000000   // workers will sit here waiting for task
                           // assignments from the master
    quit

The major functions in the scripts are included in abbreviated form here:

    // this is the function that directs the search
    function search
        int   i, chosen, bs_a, bs_b, bs_c, bs_d
        float a, b, c, d

        init_search
        init_farm

         for (i = 0; i < individuals; i = i + 1)
           // pick random individual from population
           chosen = {rand 0 population}
           if (chosen >= actual_population)
               // pick a random bit string
               bs_a = {rand 0 65536}
               bs_b = {rand 0 65536}
               bs_c = {rand 0 65536}
               bs_d = {rand 0 65536}
           else
               // mutate an existing bit string
               bs_a = {mutate {getfield /population[{chosen}] a_value}}
               bs_b = {mutate {getfield /population[{chosen}] b_value}}
               bs_c = {mutate {getfield /population[{chosen}] c_value}}
               bs_d = {mutate {getfield /population[{chosen}] d_value}}
           end
           // assign this task to a worker
           //    (delegate_task is a routine in farm.g)
           delegate_task {i} {bs_a} {bs_b} {bs_c} {bs_d}
        end

        finish        // wait for all workers to finish
    end

    // worker_task performs the task that the master has doled out to this node
    function worker_task (index, bs_a, bs_b, bs_c, bs_d)
           int index, bs_a, bs_b, bs_c, bs_d;
           float a, b, c, d;
           float fit

           // compute the real-valued parameters from the bit-strings (the
           //    "genes")
           a = ({gray_decode {bs_a}} - 32768.0) / 1000.0;
           b = ({gray_decode {bs_b}} - 32768.0) / 1000.0;
           c = ({gray_decode {bs_c}} - 32768.0) / 1000.0;
           d = ({gray_decode {bs_d}} - 32768.0) / 1000.0;

           // determine that fitness value for this individual
           fit = {evaluate {a} {b} {c} {d}}

           // return result to the master (node 0 in zone 0)
           return_result@0.0 {mytotalnode} {index} {bs_a} {bs_b} {bs_c} {bs_d} {fit}
    end

    // the workers call return_result on the master to provide the results
    //     of their evaluation
    function return_result (node, index, bs_a, bs_b, bs_c, bs_d, fit)
           int node, index, bs_a, bs_b, bs_c, bs_d
           float fit

           if (actual_population < population)
               // if we are underpopulated, each new individual stays "alive"
               least_fit = actual_population
               min_fitness = -1e+10
               actual_population = actual_population + 1
           end
           if (fit > min_fitness)
               // replace the least fit individual with the current individual
               setfield /population[{least_fit}] fitness {fit}
               setfield /population[{least_fit}] a_value {bs_a}
               setfield /population[{least_fit}] b_value {bs_b}
               setfield /population[{least_fit}] c_value {bs_c}
               setfield /population[{least_fit}] d_value {bs_d}
               recompute_fitness_extremes
           end

           // that node is now free, and can be assigned a new task
           setfield /free[{node}] value 1

           // print out the fitness value of the individual we just evaluated
           ...
    end

To run this example on your own workstation:

1. "`cd`" to the `Scripts/param` subdirectory of the directory in which PGENESIS is installed on your machine.
2. For MPI-based PGENESIS, run this example by typing "`pgenesis -nodes 4 demo.g`"; for PVM-based PGENESIS, run this example by typing "pgenesis demo.g". This will print out the fitness value of each new individual upon completion of its evaluation. Note how the values improve over time.