avatarThe Pragmatic Programmers

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

4578

Abstract

ion must return a population represented as a list of chromosomes. You also know that it must accept some function which produces an encoding of a single solution.</p><p id="4ccf">Above the run function, define the initialize function as follows:</p><div id="f34b"><pre>​ ​def​ initialize(geno<span class="hljs-keyword">type</span>) ​do​ ​ for _ <- <span class="hljs-number">1.</span><span class="hljs-number">.100</span>, ​do​: geno<span class="hljs-keyword">type</span>.() ​ ​end​</pre></div><p id="76d4">This function is a list comprehension that generates chromosomes using the provided genotype/0 function. You might be wondering why this function is called genotype. You’ll learn more about this in Chapter 3, <a href="https://readmedium.com/chapter-3-encoding-problems-and-solutions-3daf351efbac"><i>Encoding Problems and Solutions</i></a>. 1..100 is a range that creates a total of 100 chromosomes. Remember, your population can be any size. You can change this depending on your problem. The function returns a list of 100 chromosomes.</p><h1 id="0215">Evaluation</h1><p id="6139">The next step is evaluation. Your rules for evaluation require you to create a function that sorts the provided population based on a provided fitness function. Based on how you define the transformations in the outline of your algorithm, the first parameter of the function should be a population.</p><p id="e8ef">Below initialize/1, create a new function named evaluate:</p><div id="9f68"><pre>​ ​def​ evaluate(population, fitness_function) ​<span class="hljs-built_in">do</span>​ ​ population ​ |<span class="hljs-type">> Enum</span>.sort_by(fitness_function, &>=/<span class="hljs-number">2</span>) ​ ​<span class="hljs-keyword">end</span></pre></div><p id="3678">This function is identical to the evaluate function that you defined in Chapter 1, <a href="https://readmedium.com/chapter-1-writing-your-first-genetic-algorithm-e23c1f1ec785"><i>Writing Your First Genetic Algorithm</i></a>. However, rather than sorting chromosomes by sum, you sort chromosomes based on the provided fitness_function.</p><h1 id="8434">Selection</h1><p id="d13a">Now that you have a sorted population, you can define the selection step. Remember that your selection function needs to return an enumerable of tuples. For now, it doesn’t take any inputs besides population.</p><p id="46fc">Below evaluate/2, define the select/1 function, like this:</p><div id="a950"><pre>​ ​def​ <span class="hljs-keyword">select</span>(population) ​<span class="hljs-keyword">do</span>​ ​ population ​ |> <span class="hljs-keyword">Enum</span>.chunk_every(<span class="hljs-number">2</span>) ​ |> <span class="hljs-keyword">Enum</span>.map(&List.to_tuple(&<span class="hljs-number">1</span>)) ​ ​<span class="hljs-keyword">end</span></pre></div><p id="7397">This function is identical to the select function you defined in Chapter 1, <a href="https://readmedium.com/chapter-1-writing-your-first-genetic-algorithm-e23c1f1ec785"><i>Writing Your First Genetic Algorithm</i></a>. The resulting population is an enumerable of tuples. This makes it easier to implement crossover.</p><h1 id="0bff">Crossover</h1><p id="1b46">At this point, you performed initialization, evaluation, and selection. The population is transformed and ready for recombination.</p><p id="9cee">Below select/1, add the following:</p><div id="e971"><pre>​ ​def​ crossover(population) ​<span class="hljs-built_in">do</span>​ ​ population ​ |<span class="hljs-type">> Enum</span>.reduce([], ​ ​fn​ {p1, p2}, acc -> ​ cx_point = ​:rand​.uniform(length(p1)) ​ {{h1, t1}, {h2, t2}} = ​ {Enum.<span class="hljs-built_in">split</span>(p1, cx_point), ​ Enum.<span class="hljs-built_in">split</span>(p2, cx_point)} ​ {c1, c2} = {h1 ++ t2, h2 ++ t1} ​ [c1, c2 | <span class="hljs-type">acc</span>] ​ ​<span class="hljs-keyword">end</span>​ ​ ) ​ ​<span class="hljs-keyword">end</span></pre></div><p id="254b">This function should look similar to the crossover method in the previous chapter. The only difference is that :rand.uniform/1 accepts the length of one of the parents as input. This is done so the algorithm can work on chromosomes of any length. Other than that, the algorithm is identical. It splits the parents at the crossover point, creates two new children, and prepends them to the population.</p><h1 id="b461">Mutation</h1><p id="40eb">The final step in the algorithm is mutation. Mutation has no special rules. It shou

Options

ld look identical to the mutation function you defined in Chapter 1, <a href="https://readmedium.com/chapter-1-writing-your-first-genetic-algorithm-e23c1f1ec785"><i>Writing Your First Genetic Algorithm</i></a>.</p><p id="40ca">Below crossover/1, add the following:</p><div id="2eb7"><pre>​ ​<span class="hljs-function"><span class="hljs-keyword">def</span><span class="hljs-title">mutation</span></span>(population) ​<span class="hljs-keyword">do</span>​ ​ population ​ |> <span class="hljs-title class_">Enum</span>.map( ​ ​<span class="hljs-keyword">fn</span>​ chromosome -> ​ ​<span class="hljs-keyword">if</span>​ ​<span class="hljs-symbol">:rand</span>​.uniform() < <span class="hljs-number">0.05</span><span class="hljs-keyword">do</span>​ ​ <span class="hljs-title class_">Enum</span>.shuffle(chromosome) ​ ​<span class="hljs-keyword">else</span>​ ​ chromosome ​ ​<span class="hljs-keyword">end</span>​ ​ ​<span class="hljs-keyword">end</span>​ ​ ) ​ ​<span class="hljs-keyword">end</span></pre></div><p id="0a8f">You iterate over every chromosome in the population, checking to see if some condition is met. The condition is meant to model picking chromosomes at random with a probability of 5%. If a chromosome is picked, its genes are shuffled. If it isn’t, it remains the same.</p><p id="71e0">With that, the basic steps of your algorithm are complete.</p><h1 id="ff3d">Filling in the Blanks</h1><p id="8aa1">Remember the blanks you left in the evolve and run functions? Also, did you notice that none of the functions you called in evolve took parameters? It’s time to fill in these blanks.</p><p id="e2b3">The functions initialize/2 and evaluate/2 take extra parameters aside from a population. Because you won’t be calling these functions individually outside the module, you can take these parameters inside the run and evolve functions and pass them to initialize and evaluate from there. You need to take the following parameters into both run and evaluate: fitness_function, genotype, and max_fitness.</p><p id="e8b8">Additionally, with the required parameters in place, you can fill in the blanks left in the outline of the run and evolve functions.</p><p id="e1d2">Edit the functions so they look like this:</p><div id="81fd"><pre>​ ​def​ run(fitness_function, genotype, max_fitness) ​<span class="hljs-built_in">do</span>​ ​ population = initialize(genotype) ​ population ​ |<span class="hljs-type">> evolve</span>(fitness_function, genotype, max_fitness) ​ ​<span class="hljs-keyword">end</span>​ ​ ​def​ evolve(population, fitness_function, genotype, max_fitness) ​<span class="hljs-built_in">do</span>​ ​ population = evaluate(population, fitness_function) ​ best = hd(population) ​ IO.write(​<span class="hljs-string">"​​\rCurrent Best: ​​#{​fitness_function.(best)​}​​"</span>​) ​ ​<span class="hljs-keyword">if</span>​ fitness_function.(best) == max_fitness ​<span class="hljs-built_in">do</span>​ ​ best ​ ​<span class="hljs-keyword">else</span>​ ​ population ​ |<span class="hljs-type">> select</span>() ​ |<span class="hljs-type">> crossover</span>() ​ |<span class="hljs-type">> mutation</span>() ​ |<span class="hljs-type">> evolve</span>(fitness_function, genotype, max_fitness) ​ ​<span class="hljs-keyword">end</span>​ ​ ​<span class="hljs-keyword">end</span></pre></div><p id="5efa">At this point, all of the blanks are filled. Additionally, the evolve and run functions accept the required problem-specific parameters and pass them to the functions that need them.</p><p id="fc91">You now have a complete, working framework.</p><p id="5df0"><i>👈 <a href="https://readmedium.com/using-mix-to-write-genetic-algorithms-86a51b8bb191">Using Mix to Write Genetic Algorithms</a> | <a href="https://readmedium.com/table-of-contents-879fc8614df">TOC</a> | <a href="https://readmedium.com/understanding-hyperparameters-e7f2fe734610">Understanding Hyperparameters</a> 👉</i></p><p id="415f"><i>Genetic Algorithms in Elixir by Sean Moriarity can be purchased in other book formats <a href="https://pragprog.com/titles/smgaelixir">directly from the Pragmatic Programmers</a>. If you notice a code error or formatting mistake, please let us know <a href="https://readmedium.com/how-to-report-errata-4e164674347a">here</a> so that we can fix it.</i></p><figure id="04cf"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*U2E2a23SuM4520w9CUXSWw.jpeg"><figcaption></figcaption></figure></article></body>

Building a Framework for Genetic Algorithms

Genetic Algorithms in Elixir — by Sean Moriarity (23 / 101)

👈 Using Mix to Write Genetic Algorithms | TOC | Understanding Hyperparameters 👉

You now have an empty project and an idea of what your genetic algorithm framework should look like. It’s time to start implementing each step.

Start by opening the genetic.ex file. The file is populated with some default code and will look something like this:

​ ​defmoduleGeneticdo​
​   ...documentation...
​   ​defhellodo​
​     ​:world​
​   ​end​
​ 
​ ​end

You can delete the default documentation and Hello World function. This module will contain the most basic parts of your genetic algorithm. This is where you’ll define each step of the algorithm based on the rules you determined earlier.

Creating an Initial Outline

In genetic.ex, start by creating a function named run, like this:

​ ​defrun(...) ​do​
​   population = initialize()population
​   |> evolve()
​ ​end

This function calls the initialize function, which is responsible for creating the initial population. You might be wondering why the parameters of run are left blank. You’ll worry about them later. For now, concentrate on the overall structure of the code.

After you initialize the population, you pass the population into a function named evolve. evolve is designed to model a single evolution in your genetic algorithm. Below the run function, define evolve, like this:

​ ​def​ evolve(population, max_fitness) ​do​
​   population = evaluate(population, ..., opts)
​   best = hd(population)
​   IO.write(​"​​\rCurrent Best: ..."​)
​   ​if​ ... == max_fitness ​do​
​     best
​   ​else​
​     population
​     |> select()
​     |> crossover()
​     |> mutation()
​     |> evolve()
​   ​end​
​ ​end

For now, disregard the blank parts of the code. You’ll fill these in at the end. This function evaluates the population and then extracts the “best” solution from the population. You can do this using the hd/1 function because one of the rules for evaluate was that it returns a sorted population. This means the fittest chromosome will always be the first one in the population.

This function also implements the recursion you need. It defines the base or termination case. Part of the termination criteria is left blank for now. It checks some blank value versus the maximum desired fitness. The recursive case is essentially identical to the recursive case from Chapter 1, Writing Your First Genetic Algorithm.

With the basic outline defined, you can start implementing each step.

Initialization

Remember the rules you defined in the previous section? You’ll use them now to implement each step correctly. Start with the initialize function. Based on the design rules discussed previously, you know the function must return a population represented as a list of chromosomes. You also know that it must accept some function which produces an encoding of a single solution.

Above the run function, define the initialize function as follows:

​ ​def​ initialize(genotype) ​do​
​   for _ <- 1..100, ​do​: genotype.()
​ ​end​

This function is a list comprehension that generates chromosomes using the provided genotype/0 function. You might be wondering why this function is called genotype. You’ll learn more about this in Chapter 3, Encoding Problems and Solutions. 1..100 is a range that creates a total of 100 chromosomes. Remember, your population can be any size. You can change this depending on your problem. The function returns a list of 100 chromosomes.

Evaluation

The next step is evaluation. Your rules for evaluation require you to create a function that sorts the provided population based on a provided fitness function. Based on how you define the transformations in the outline of your algorithm, the first parameter of the function should be a population.

Below initialize/1, create a new function named evaluate:

​ ​def​ evaluate(population, fitness_function) ​do​
​   population
​   |> Enum.sort_by(fitness_function, &>=/2)
​ ​end

This function is identical to the evaluate function that you defined in Chapter 1, Writing Your First Genetic Algorithm. However, rather than sorting chromosomes by sum, you sort chromosomes based on the provided fitness_function.

Selection

Now that you have a sorted population, you can define the selection step. Remember that your selection function needs to return an enumerable of tuples. For now, it doesn’t take any inputs besides population.

Below evaluate/2, define the select/1 function, like this:

​ ​def​ select(population) ​do​
​   population
​   |> Enum.chunk_every(2)
​   |> Enum.map(&List.to_tuple(&1))
​ ​end

This function is identical to the select function you defined in Chapter 1, Writing Your First Genetic Algorithm. The resulting population is an enumerable of tuples. This makes it easier to implement crossover.

Crossover

At this point, you performed initialization, evaluation, and selection. The population is transformed and ready for recombination.

Below select/1, add the following:

​ ​def​ crossover(population) ​do​
​   population
​   |> Enum.reduce([],
​       ​fn​ {p1, p2}, acc ->
​         cx_point = ​:rand​.uniform(length(p1))
​         {{h1, t1}, {h2, t2}} =
​           {Enum.split(p1, cx_point),
​           Enum.split(p2, cx_point)}
​         {c1, c2} = {h1 ++ t2, h2 ++ t1}
​         [c1, c2 | acc]
​       ​end​
​     )
​ ​end

This function should look similar to the crossover method in the previous chapter. The only difference is that :rand.uniform/1 accepts the length of one of the parents as input. This is done so the algorithm can work on chromosomes of any length. Other than that, the algorithm is identical. It splits the parents at the crossover point, creates two new children, and prepends them to the population.

Mutation

The final step in the algorithm is mutation. Mutation has no special rules. It should look identical to the mutation function you defined in Chapter 1, Writing Your First Genetic Algorithm.

Below crossover/1, add the following:

​ ​defmutation(population) ​do​
​   population
​   |> Enum.map(
​       ​fn​ chromosome ->
​         ​if​ ​:rand​.uniform() < 0.05do​
​           Enum.shuffle(chromosome)
​         ​else​
​           chromosome
​         ​end​
​       ​end​
​     )
​ ​end

You iterate over every chromosome in the population, checking to see if some condition is met. The condition is meant to model picking chromosomes at random with a probability of 5%. If a chromosome is picked, its genes are shuffled. If it isn’t, it remains the same.

With that, the basic steps of your algorithm are complete.

Filling in the Blanks

Remember the blanks you left in the evolve and run functions? Also, did you notice that none of the functions you called in evolve took parameters? It’s time to fill in these blanks.

The functions initialize/2 and evaluate/2 take extra parameters aside from a population. Because you won’t be calling these functions individually outside the module, you can take these parameters inside the run and evolve functions and pass them to initialize and evaluate from there. You need to take the following parameters into both run and evaluate: fitness_function, genotype, and max_fitness.

Additionally, with the required parameters in place, you can fill in the blanks left in the outline of the run and evolve functions.

Edit the functions so they look like this:

​ ​def​ run(fitness_function, genotype, max_fitness) ​do​
​   population = initialize(genotype)
​   population
​   |> evolve(fitness_function, genotype, max_fitness)
​ ​end​
​ ​def​ evolve(population, fitness_function, genotype, max_fitness) ​do​
​   population = evaluate(population, fitness_function)
​   best = hd(population)
​   IO.write(​"​​\rCurrent Best: ​​#{​fitness_function.(best)​}​​"​)
​   ​if​ fitness_function.(best) == max_fitness ​do​
​     best
​   ​else​
​     population
​     |> select()
​     |> crossover()
​     |> mutation()
​     |> evolve(fitness_function, genotype, max_fitness)
​   ​end​
​ ​end

At this point, all of the blanks are filled. Additionally, the evolve and run functions accept the required problem-specific parameters and pass them to the functions that need them.

You now have a complete, working framework.

👈 Using Mix to Write Genetic Algorithms | TOC | Understanding Hyperparameters 👉

Genetic Algorithms in Elixir by Sean Moriarity can be purchased in other book formats directly from the Pragmatic Programmers. If you notice a code error or formatting mistake, please let us know here so that we can fix it.

Smgaelixir
Recommended from ReadMedium