Genetic Algorithm (GA): A Simple and Intuitive Guide
By: Dana Bani-Hani
Tags: Metaheuristics, Editors Pick, Data Science, Optimization, Genetic Algorithm

So, What is Genetic Algorithm (GA)?

GA is a population-based metaheuristic developed by John Holland in the 1970s. GA uses techniques inspired from nature, more specifically evolution, to find an optimal or near-optimal solution towards a problem. It applies evolution concepts such as reproduction and survival of the fittest to solve a problem. GA belongs to the larger class of evolutionary algorithms.

If I were to try and explain GA in a few sentences, I would say that GA is a way to optimize a problem by creating many solutions and updating those solutions in certain ways related to evolution concepts to reach a “good enough” solution or the best solution possible.

In continuous problems, where the decision variable you want to find the optimal value for takes on real numbers, a solution in GA would look like what is shown in Figure 8. This is called a chromosome (sometimes referred to as a string). Each square below is called a “gene” while the value each gene takes is called an “allele”.

Figure 8: an example of a chromosome in GA (image by author)

In real-life problems, most decision variables are constrained and bounded, so that it does not take a value of infinity (which is not a real number). For example, if you had an optimization problem that wanted to maximize the profit (or minimize the cost) for a certain company, it may ask you what is the maximum number of labor hours we can set, usually given other conditions. You may be first inclined to say that we can have an infinite number of hours, but since that is not feasible, the company would tend to set a maximum limit per month, for example, it may say that the number of hours per week should not exceed 120, therefore, the lower bound of the number of hours (x) would be 0 and the upper bound would be 120 (0≤x≤120).

Now that we can place the decision variable x between bounds, calculating a real number for the hours can be found.

Figure 9: each gene with its corresponding 2^i (image by author)

For the chromosome in Figure 9, or any other GA chromosome encoded with 0s and 1s (in the case of continuous problems), each gene will be represented with 2^i (2 to the power of i, where i starts at 0, on the far right, and is increased by 1 the more you have genes in the chromosome, i.e. 2⁰, 2¹, 2², 2³, …, 2^i).

To decode this chromosome (transform it to a real value), we use Formula 1, where the summation of bits * 2^i is calculated by summing the multiplications of each value in the gene with its corresponding 2^i. By looking at Figure 9, and starting from the right, this would be equal to (0*2⁰)+(1*2¹)+(0*2²)+(0*2³)+(0*2⁴)+(1*2⁵)+(0*2⁶)+(1*2⁷)+(1*2⁸), this would give us a value of 418.

Formula 1: decoding a GA chromosome

For the second part of Formula 1, the precision, the equation is given in Formula 2, below, where l is the length of the chromosome, in our case it would be 9, since we have 9 genes, b is the upper bound, which we have as 120 (given in the example as the maximum number of hours allowed), and a is the lower bound which is 0. The precision now would be (120–0)/(2⁹-1) which is equal to 0.234833, therefore decoding the chromosome would be 418*0.234833+0 which is 98.16 hours — a value between 0 and 120.

Formula 2: GA precision

Important note: if we were to replace all the genes in our 9-gene chromosome with 1s, we should get the maximum value allowed which is 120, as defined by the upper bound, and if we were to replace all the genes with 0s, we should get the minimum allowed which is 0, as defined by the lower bound. Let’s try decoding the chromosome when we have all 1s. Our summation of bits multiplied by 2^i would be (1*2⁰)+(1*2¹)+(1*2²)+(1*2³)+(1*2⁴)+(1*2⁵)+(1*2⁶)+(1*2⁷)+(1*2⁸), see, no zeros, that would give us a total of 511. If we multiply 511 with the precision 0.234833, and add a (which is 0), we would get 120. Note: you will actually get 119.99963 because the precision 0.234833 has more digits but I only took the first 6 after the decimal point, however, if you include all digits, you should get exactly 120.

The chromosome representation (encoding) is called a “genotype” while the decoded value of the chromosome is called a “phenotype”.

Note: the length of the chromosome (l) is set by the user. The longer the string the more accurate your decoded value is, but there is a trade-off with computational time.

What if we had more than one decision variable?

Say we had a minimization problem that we need to find the best values for x and y that would give us the lowest f(x). How would you represent that? Simple, we will have a chromosome where half of the genes represent x, and the other half represent y. Keep in mind that for each x and y, the gene that represents the start of each one would have a corresponding value of 2⁰.

See Figure 10, where the green genes represent x and the blue genes represent y, they both occupy the same string but they should be treated separately when decoding each to get its corresponding value. You decode each the exact same way shown above. The same principle applies for when you have more than two decision variables.

Figure 10: a chromosome with 2 decision variable (x and y) (image by author)

Terminologies you need to understand before going deeper into GA:

Besides the terminologies above such as chromosome, genes, alleles, genotype, and phenotype, here I explain a few others, where their explanations might intertwine with each other:

1- Population: when we say population, it means a set of solutions at a certain generation (gen). Remember that GA is a population-based search algorithm, this means that at every time (gen), we have several solutions in our search space. All have the potential of updating and becoming new solutions. In GA, the population size remains constant throughout the search, so if your population size is 100, you will have 100 solutions in each gen, each is a chromosome or string that can be decoded to a certain value.

Figure 11: a population size of 10, with the first 4 shown to the reader (image by author)

Figure 11 shows an example of a population size of 10 (population size is usually much higher than 10, but I used 10 for illustration purposes), each with a chromosome size of 8. All of the 10 chromosomes are filled with 0s and 1s (I only show the first 4 filled, also for illustration purposes, but they will all be filled with 0s and 1s).

Important note: the initial population that you want to start your algorithm with is chosen at random. You initially set the length of the chromosome and how many chromosomes you want in the population (population size), and randomly fill in 0s and 1s, this is the case for your first generation. The population in the subsequent generation is the result of manipulating and updating the population from the previous generation, i.e. create a random population for gen #1, the resulting offspring becomes the population for gen #2, then their offspring become the population for gen #3, and so on.

2- Generation: your algorithm will run for gen generations before terminating. Each generation will have the same population size but with different solutions (some might stay the same as the previous generation). Before going to the next generation (e.g. from gen #1 to gen #2), you will need to update your current population with the crossover and mutation operators to create new solutions (offspring) in the search space. After you get a new set of solutions equal to the population size, you then go to the next generation and start over using the new solutions as your population.

3- Parents and offspring: in each generation, your current population are all potential parents that have some chance to “mate” and produce “offspring”, i.e. a chance to become a parent. These offspring are the new set of solutions that will go on to become the population for the next generation, then their offspring will become the population for the generation after that, and so on. Think of it as how we have generations in real life.

How Do We Update the Population and Create Offspring?

GA is inspired by evolution and survival of the fittest, which means that in order to produce offspring, 2 chromosomes or “parents” will reproduce and create 2 offspring or “children”.

Parent selection:

Since we are looking for good or the best solution(s) to a problem, we need to try and make our new and updated solutions come from good ones.

There will be a parent selection method that we will apply to the population to find 2 good solutions, which will be the 2 parents, and apply an operator called crossover on the selected parents to produce the children. So how can we make sure we select 2 good solutions to be the parents?

First, you need to know that just because a solution (chromosome) is not good compared to the rest in the population, it does not mean it will not get picked as a parent, it only means that it has a lower chance of getting picked. There are many parent selection methods developed, but the two widely used are tournament selection and roulette wheel selection.

1- Tournament selection: the idea is straightforward, you randomly select k solutions from the population (with or without replacement), where you set the value k, and pick the best one to be parent #1, then repeat by picking another k solutions and choosing the best one to be parent #2. Simple, right? But how do you know which is best? You know by comparing their corresponding objective function value as to say which chromosome is the best choice for your f(x).

Example: let’s take the Booth function f(x,y)=(x+2y-7)²+(2x+y-5)² as a minimization problem in which x and y are both bounded by -10≤x,y≤10, where -10 and 10 are the lower bound (a) and upper bound (b), respectively. We want to know what are the values of x and y that would give us the lowest f(x).

Figure 12: parent #1 selection using the tournament selection method (image by author)

Now we want to select parents to produce offspring, and we set k=3. We randomly select 3 chromosomes from the population we have, then we decode each chromosome to the actual values of x and y. Starting with the first randomly selected chromosome, if x after decoding was 2.1, for example, and y after decoding was 3.6, we plug these values in f(x) and we get f(x)=(2.1+2*3.6-7)²+(2*2.1+3.6–5)², which is equal to 13.13. Then we try another chromosome that was selected and find that, through the same procedure, its f(x) is, for example, 8.32, then the last chromosome we selected and its f(x) is 16.84. Since it is a minimization problem, we need the lowest f(x) as the better solution, therefore, the chromosome that gave us 8.32 outperformed the other two that gave us 13.13 and 16.84, so this would be parent #1, this is illustrated in Figure 12. And to find parent #2, we repeat the exact thing. f(x) for any chromosome is called the fitness, fitness value, or objective function value.

So, the tournament selection method is applied using the following steps:

Select k solutions at random

Find f(x) for each k chromosome

Pick the chromosome that gives the best f(x), if more than one solution tied for best, pick any

Set as parent #1

Repeat to find parent #2

2- Roulette wheel selection: the fitness (f(x)) of a chromosome is used to associate a probability to select the parents. For maximization problems, the higher f(x), the higher the probability it will get to be selected. See Figure 13 as an example, there are 5 solutions in the population that we need to select from. The probability is based on how good the solution is compared to the rest, the higher the probability, the higher the chance of getting selected, which is the survival of the fittest theory but still giving some chance for worse solutions to be selected.

Figure 13: roulette wheel selection method (image by NCL)

Figure 14: cumulative sum of probabilities (image by author)

To get the probabilities for each solution, you add all the fitness values (f(x)) together to get the total and divide each f(x) by the total to get a percentage of how much it occupies from the total. Then, you select a random number, which is between 0 and 1, and select the solution to be parent #1 from the probabilities set. If the 5 solutions get probabilities 5%, 12%, 14%, 31%, and 38%, based on their fitness, and your random number comes out to be 0.89, then it will be in the range of 0.62 and 1.00 which would make us choose the chromosome #3, which had a probability of 38% of being selected. If our random number came out to be 0.16, then we would choose chromosome #4, which has the probability of 12%. As you can see in Figure 14, the range is based on the cumulative sum of the probabilities.

Table 1: example of 5 chromosomes and their corresponding probabilities (table by author)

Another example: if we had 5 chromosomes, and their fitness values were as following: #1: 3.12, #2: 5.18, #3: 6.46, #4: 4.23, and #5: 3.66, for a maximization problem, we first add them to get their total which is 22.65. Now, we divide each one to the total (i.e. 3.12/22.65) to get percentages. We would get 0.13775, 0.2287, 0.28521, 0.18675, and 0.16159, respectively, which when you add up, should give you 1.00. Afterwards, we find the cumulative probability, this allows us to create a range from which our random number will select from. In Table 1, the probability for chromosome #1 to be chosen as a parent is if we get a random number to be between 0 and 0.137748344, and to get chromosome #4, the random number should be higher than 0.651655629 and lower than 0.838410596.

Okay, but what happens if we have a minimization problem?

With maximization problems, the probability of a chromosome is relative to its fitness value which makes the higher the fitness, the better, and the higher the probability. But if we had a minimization problem, the smaller fitness the better but we need to associate it with a higher probability, how? There are many ways you can do this, one way I choose is to take the invert of each fitness value, f(x), so that when you take the invert of the lowest number and compare it to the invert of the others, it would be the highest.

For example, if we had a minimization problem and the fitness values for 3 chromosomes are 2, 4, and 6. We would like 2 (the lowest f(x)) to get the highest probability and 6 to get the lowest. So, we invert them: 2 would become 1/2 (0.5), 4 would become 1/4 (0.25), and 6 would become 1/6 (0.16667). Add 0.5 to 0.25 to 0.16667, now the fitness total is 0.91667. Therefore, the associated probabilities would be 0.54545, 0.27273, and 0.18182, respectively. You can see that 2, the lowest f(x), has the highest probability of 0.54545.

Crossover:

Now that we have 2 parents, the next step is to perform the crossover operator and create offspring or children. There are many ways to do crossover, here I will explain the two methods commonly used:

Important note: I will be using letters instead of 0s and 1s to show you how it works because explaining with 0s and 1s could cause confusion.

#1- Single-point crossover:

Figure 15: single-point crossover example (image by author)

When we have the 2 parents, we apply the single-point crossover by randomly selecting a cutoff point. You can do this by selecting a random integer to specify where the crossover will happen. Example, let’s say that our random integer came out to be 6 (this depends on what programming language you will use as Python, for example, starts indexing at 0). So the genes after the cutoff point will be exchanged between the two parents. See Figure 15, the [R, Z, P] from parent #1 will interchange with the [A, F, W] from parent #2 and this will create the offspring: child #1 and child #2.

#2- Two-point crossover:

Figure 16: two-point crossover example (image by author)

The idea is the same as single-point crossover but instead of randomly selecting one integer, we select two and exchange what is in between. In Figure 16, the two-point crossover made us exchange [T, B, L] with [P, S, D] to create the offspring. You can either have the two random integers differ from each other by an if or while statement, or you can allow the integers, by chance, to be the same, but if this happens, it will become a single-point crossover. This is based on the user’s preference.

Note: the crossover operator can be done based on a probability, so that you won’t preform crossover every time, however, most studies tend to apply a probability of 1 (100%) with crossover ensuring the operator will happen after every time parents are selected.

Mutation:

To create diversity in the search space, the mutation operator is introduced at a certain probability (usually low, e.g. 0.1 or 0.01). Mutation is applied on the children after creating them from crossing over the two parents.

Figure 17: mutation example (image by author)

The way to do this is straightforward: you start with child #1 and go through each gene one by one. You start at the first gene, and get a random number, if the random number is less than the probability of mutation, you mutate, else, move to the next gene and apply another random number and so on. But what happens when we do mutate? Simple, if the gene you want to mutate has a 0 as its value, it would become 1, and if the gene had the value of 1, it would become a 0. Look at Figure 17, the red boxes indicate which genes will be mutated and the green boxes show the result of mutation. Since the probability of mutation is low, not many (or even zero) genes will mutate.

Putting it All Together

The below flowchart, Figure 18, is a flowchart I personally developed for GA when using tournament selection as a parent selection method. I found almost all of the flowcharts that are online a bit difficult for beginners to understand. Notations in the flowchart are as following: M is the number of generations, N is the population size, p_c is the probability of crossover, p_m is the probability of mutation, and k is the number of chromosomes selected at random during tournament selection.

Figure 18: GA flowchart with tournament selection (image by author)

At first, we select the number of generations (M), population size (N), probability of crossover (p_c), probability of mutation (p_m), and number of chromosomes to be selected for tournament selection (k). Let’s say we set M=100, N=120, p_c=1.00, p_m=0.10, and k=3. Our problem we want to solve is a continuous minimization problem.

We randomly create a population of 120 chromosomes (each chromosome is randomly created with 0s and 1s, at a length (l) you choose). At this moment, we are at gen #1. Time to select 2 parents using tournament selection. We randomly select 3 solutions from the population and we find the one with the best fitness value (lowest f(x), since we are dealing with a minimization problem), and that will be parent #1. We then do this again by selecting 3 random solutions and finding the best one, that would be parent #2. At p_c, we crossover the 2 parents to get 2 children. At p_m, we mutate the 2 children to get 2 mutated children. We store these 2 mutated children in an array. Do steps 2 to 5 N/2 times (60 times) — 60 because 120/2 is 60. We need to recreate a population of 120 for each future generation to keep the population size consistent. Because each time we produce 2 children or 2 mutated children, we need to perform the steps 2 to 5 60 times to get a population of 120 for the next generation (120*2). Store ALL resulted mutated children in an array. At this point we should have a population of 120, which are the mutated offspring of gen #1. We move to the next generation, gen #2, and repeat steps 2 to 6. The resulted offspring will now become the population of gen #3. Keep doing this till you finish all 100 (M) generations. You have two options: 1) either select the best solution (mutated child chromosome) out of gen #100 (the converged solution: the solution your algorithm converged to right before termination), or, 2) out of each generation’s mutated children, right before you go to the next generation, store the best mutated child in a separate array. You do this for all generations so that at the end of your algorithm, you will have an array that has the best solution for each generation which would have the best 100 solutions your algorithm has seen. Then pick the best out of these 100 chromosomes to be your final solution — I, myself, choose to go with option #2 because this way, you get to make sure you have the best answer the algorithm has come across because in the array, solution #100 is the same solution if you ended up picking what the algorithm converged to because when you select the answer the algorithm converged to (option #1), you are selecting the best of only gen #100 which is the same as the array at #100. This way, you make sure you select the best from all 100 generations rather than just the final generation. Because it is extremely likely you may reach the best possible solution at an earlier generation (e.g. gen #42) but then the GA algorithm will start to go to other places in the search space that are worse than what you have at gen #42, and end up converging to a bad solution. Now that you have the best chromosome, decode it to get real values and there you go, you solved the problem!

Congratulations! You now know what GA is and how it works!

Summary

GA is a powerful population-based search metaheuristic algorithm. It is inspired by evolution and its concepts such as reproduction and survival of the fittest.

In this explanation, I covered how GA is applied to continuous optimization problems where the chromosomes are represented (encoded) with 0s and 1s. We learnt how to decode a chromosome into an actual value and how to select parents, perform crossover to produce children, and how to mutate them. A flowchart and a step by step guide on how the GA algorithm is executed have also been thoroughly explained.

Final note: the same principles of crossover and mutation can be applied to combinatorial problems, such as the TSP, where what you want to optimize are not decision variables that take on real numbers but discrete elements, such as traveler or vehicle routes. Although the idea behind crossover and mutation would be the same for combinatorial problems, the way we apply them is slightly different, because you do not want to end up with a traveler’s path like [A, D, B, D], where city D is mentioned twice and city C is not included in the path, and this may happen if you use the continuous crossover or mutation methods on discrete problems.

© 2020 Dana Bani-Hani — articles, codes, and images developed by Dana Bani-Hani should not be used without permission and proper citation.

References

Holland, John Henry. Adaptation in natural and artificial systems: an introductory analysis with applications to biology, control, and artificial intelligence. MIT press, 1992.

Intro Image: https://www.freepik.com/free-photo/dna-bacterias_923816.htm#page=3&query=dna+genetics&position=21

Figure 2: https://www.anotherbookondatascience.com/chapter6.html

Figure 5: https://www.mathworks.com/matlabcentral/mlc-downloads/downloads/submissions/27178/versions/6/previews/html/gaPeaksExample.html

Figure 6: https://www.cs.cmu.edu/afs/cs/project/jair/pub/volume15/ambite01a-html/node9.html

Figure 13: http://www.edc.ncl.ac.uk/highlight/rhjanuary2007g02.php