Optimizing Spark Configuration with Genetic Algorithm - Operators

Optimizing Spark Configuration with Genetic Algorithm - Operators

In the realms of science and engineering, genetic algorithms serve dual purposes: as adaptable algorithms addressing real-world challenges and as computational representations of natural evolutionary mechanisms.?

This article stands as the subsequent chapter in our series on genetic algorithms. Within, we detail the Scala-based implementation of genetic operators, encompassing selection, cross-over, and mutation, acting upon a chromosome population.


Introduction

In the first article on genetic algorithms, you learned about the key elements of genetic algorithms such as Chromosomes, Genes, Quantization, Population - Optimizing Spark Configuration with Genetic Algorithm - Encoding

This second part introduces the concept and implements of genetic operations (cross-over, mutation and selection) on a population of chromosomes. These operators are applied recursively on each chromosome and each of the genes it contains.

The 3rd and final post on genetic algorithms, explores the application of genetic algorithm as a solver or optimizer?Optimizing Spark Configuration with Genetic Algorithm - Evaluation


Note:?For the sake of readability of the implementation of algorithms, all non-essential code such as error checking, comments, exception, validation of class and method arguments, scoping qualifiers or import is omitted.


Selection

The first genetic operator of the reproduction cycle is the selection process. The?select?method of the class?Population?implements the steps of the selection of the fittest chromosomes in the population in the most efficient manner, as follows:

def select(score: Chromosome[T] => Unit, cutOff: Double) = {
   val cumul = chromosomes./:(0.0)(
      (s,x) =>{ score(xy); s + xy. unfitness}       //  # 1
   ) 

   chromosomes foreach( _ /= cumul)         //   #2
   val newChromosomes = chromosomes.sortWith(_.unfitness < _.unfitness)   // #3
   val cutOffSize = (cutOff*newChromosomes.size).floor.toInt      //   #4
   val newPopSize = if(limit<cutOffSize) limit else cutOffSize        //    #5

   chromosomes.clear
   chromosome        

The?select?method computes the cumulative sum of an unfitness value,?cumul,?for the entire population (# 1). It normalizes the unfitness of each chromosome (#2), orders the population by decreasing value (# 3), and applies a soft limit function on population growth,?cutOff?(#4). The last step reduces the size of the population to the lowest of the two limits: the hard limit,?limit,?or the soft limit, cutOffSize?(# 5).

Cross-over

There are several options to select pairs of chromosomes for crossover. This implementation ranks the chromosomes by their?fitness?value and then divides the population into two halves. Finally, it pairs the chromosomes of identical rank from each half as illustrated in the following diagram:?

Fig 1 Visualization of selection and cross-over algorithm

Population cross-over

The crossover implementation,?+-?, selects the parent chromosome candidates for crossover using the pairing scheme described earlier.

def +- (xOver: Double): Unit = {
   if( size > 1) {
      val mid = size>>1          //   # 1
      val bottom = chromosomes.slice(mid, size)        //    #2
   
      val gIdx = geneticIndices(xOver)                 //   #3
      val offSprings = chromosomes.take(mid)      //   #4
             .zip(bottom)
             .map(p => p._1 +-(p._2, gIdx))
             .unzip
      chromosomes ++= offSprings._1 ++ offSprings._2        //    #5
   }
}        

The implementation of the cross-over on the population of chromosomes ranked by their?unfitness?consists of?

  • Get the mid point of the list of ranked chromosomes (# 1)
  • Get the least fit half of the chromosome (# 2)
  • Retrieve the position of the bit in the chromosome the cross-over applies (# 3)
  • Retrieve the two offspring by crossing over pairs of chromosomes from each half of the ranked population (# 4)
  • Add the two off-springs to the current population (# 5)


Chromosome cross-over

The implementation of the crossover for a pair of chromosomes using hierarchical encoding follows two steps:

  • Find the gene on each chromosome that corresponds to the crossover index,?gIdx.chOpIdx,?and then swap the remaining genes (# 6)
  • Split and splice the gene crossover at?xoverIdx?(# 7)

def +-(
   that: Chromosome[T], 
   gIdx: GeneticIndices
): (Chromosome[T], Chromosome[T]) = {
 
     val xoverIdx = gIdx.chOpIdx          #6
     val xGenes = spliceGene(gIdx, that.code(xoverIdx) )
 
    val offSprng1 = code.slice(0, xoverIdx) ::: xGenes._1        //  #7
        :: that.code.drop(xoverIdx+1)

    val offSprng2 = that.code.slice(0, xoverIdx) ::: xGenes._2      //  #8
        :: code.drop(xoverIdx+1)
 
   (Chromosome[T](offSprng1), Chromosome[T](offSprng2)
}        

The crossover method computes the index of the bit that defies the crossover?xoverIdx?in each parent chromosome. The genes?code(xoverIdx)?and?that.code(xoverIdx)? are swapped and spliced by the?spliceGene?method to generate a spliced gene (# 8).The method?spliceGene?is implemented below.

def spliceGene(gIdx: GeneticIndices, thatCode: T): (T, T) = {
     ((this.code(gIdx.chOpIdx) +- (thatCode, gIdx)),
      (thatCode +- (code(gIdx.chOpIdx), gIdx)) )
}        

Gene cross-over

The crossover is applied to a gene through the?+-?method of the Gene class. The exchange of bits between the two genes this and that uses the?BitSet?Java class to rearrange the bits after the permutation (# 9). The bit string is then decoded to produce the predicate or logical representation of the gene (# 10).

def +- (that: Gene, idx: GeneticIndices): Gene = {
    val clonedBits = cloneBits(bits)
  
    Range(gIdx.geneOpIdx, bits.size).foreach(n =>     //  #9
        if( that.bits.get(n) ) clonedBits.set(n)
        else clonedBits.clear(n)
    ) 
    val valOp = decode(clonedBits)         // #10

    Gene(id, valOp._1, valOp._2)
}        

Mutationnbsp;

Population mutation

The mutation operator?^?invokes the same operator for all the chromosomes in the population and then adds the mutated chromosomes to the existing population, so that they can compete with the original chromosomes.?

The mutation parameter mu is used to compute the absolute index of the mutating gene,?geneticIndices(mu).?We use the notation?^?to define the mutation operator to remind the reader that the mutation is implemented by flipping one bit:

def ^ (mu: Double): Unit =
     chromosomes ++= chromosomes.map(_ ^ geneticIndices(mu))          

Chromosome mutation

The implementation of the mutation operator?^?on a chromosome consists of mutating the gene of the index?gIdx.chOpIdx?and then updating the list?xs?of genes in the chromosome. The method returns a new chromosome with this new generic code that is added to the population.

def ^ (gIdx: GeneticIndices): Chromosome[T] = {
    val mutated = code(gIdx.chOpIdx) ^ gIdx

    val xs = Range(0, code.size).map(
        i => if(i==gIdx.chOpIdx) mutated else code(i)
    ).toList

    Chromosome[T](xs)
}        

Gene mutation

Finally, the mutation operator flips (XOR) the bit at the index?gIdx.geneOpIdx

The?^?method mutates the cloned bit string,?clonedBits?(# 11) by flipping the bit at the index?gIdx.geneOpIdx?(# 12). It decodes (# 13) and converts the mutated bit string by converting it into a (target value, operator) tuple (# 14). The last step creates a new gene from the target-operator tuple.

def ^ (gIdx: GeneticIndices): Gene = {
     val clonedBits = cloneBits(bits)                 //  #11
     clonedBits.flip(gIdx.geneOpIdx)                  //  #12

     val valOp = decode(clonedBits)                 //  #13

     Gene(id, valOp._1, valOp._2)                     //  #14
}        

This concludes the second part of the implementation of genetic algorithms in Scala, dedicated to genetic operators.

Thank you for reading this article. For more information ...


References


---------------------------

Patrick Nicolas has over 25 years of experience in software and data engineering, architecture design and end-to-end deployment and support with extensive knowledge in machine learning.? He has been director of data engineering at Aideo Technologies since 2017 and he is the?author of "Scala for Machine Learning" Packt Publishing ISBN 978-1-78712-238-3


要查看或添加评论,请登录

社区洞察

其他会员也浏览了