mirror of
https://github.com/nature-of-code/noc-book-2
synced 2024-11-17 07:49:05 +01:00
1275 lines
No EOL
116 KiB
HTML
1275 lines
No EOL
116 KiB
HTML
<section data-type="chapter">
|
||
<h1 id="chapter-9-the-evolution-of-code">Chapter 9. The Evolution of Code</h1>
|
||
<blockquote data-type="epigraph">
|
||
<p>“The fact that life evolved out of nearly nothing, some 10 billion years after the universe evolved out of literally nothing, is a fact so staggering that I would be mad to attempt words to do it justice.”</p>
|
||
<p>— Richard Dawkins</p>
|
||
</blockquote>
|
||
<p>Let’s take a moment to think back to a simpler time, when you wrote your first p5.js sketches and life was free and easy. What is one of programming’s fundamental concepts that you likely used in those first sketches and continue to use over and over again? <em>Variables</em>. Variables allow you to save data and reuse that data while a program runs. This, of course, is nothing new to you. In fact, you have moved far beyond a sketch with just one or two variables and on to more complex data structures—variables made from custom types (objects) that include both data and functionality. You've made our own little worlds of movers and particles and vehicles and cells and trees.</p><a data-type="indexterm" data-primary="evolution" data-secondary="modeling"></a><a data-type="indexterm" data-primary="natural phenomena" data-secondary="evolution"></a>
|
||
<p>In each and every example in this book, the variables of these objects have to be initialized. Perhaps you made a whole bunch of particles with random colors and sizes or a list of vehicles all starting at the same <code>x</code>,<code>y</code> position on screen. But instead of acting as “intelligent designers” and assigning the properties of our objects through randomness or thoughtful consideration, you can let a process found in nature<code>—</code><em>evolution</em><code>—</code>decide for you.</p>
|
||
<p>Can you think of the variables of an object as its DNA? Can objects make other objects and pass down their DNA to a new generation? Can your simulation evolve?</p>
|
||
<p>The answer to all these questions is yes. After all, the book would hardly be complete without tackling a simulation of one of the most powerful algorithmic processes found in nature itself. This chapter is dedicated to examining the principles behind biological evolution and finding ways to apply those principles in code.</p>
|
||
<h2 id="91-genetic-algorithms-inspired-by-actual-events">9.1 Genetic Algorithms: Inspired by Actual Events</h2><a data-type="indexterm" data-primary="evolution" data-secondary="genetic algorithms"></a><a data-type="indexterm" data-primary="genetic algorithms"></a><a data-type="indexterm" data-primary="natural phenomena" data-secondary="genetic algorithms"></a>
|
||
<p>It’s important for me to clarify the goals of this chapter. I will not go into depth about the science of genetics and evolution as it happens in the real world. I won’t be making Punnett squares (sorry to disappoint) and there will be no discussion of nucleotides, protein synthesis, RNA, and other topics related to the actual biological processes of evolution. Instead, I am going to look at the core principles behind Darwinian evolutionary theory and develop a set of algorithms <em>inspired</em> by these principles. I don’t care so much about an accurate simulation of evolution; rather, I care about methods for applying evolutionary strategies in software.</p>
|
||
<p>This is not to say that a project with more scientific depth wouldn’t have value, and I encourage readers with a particular interest in this topic to explore possibilities for expanding the examples provided with additional evolutionary features. Nevertheless, for the sake of keeping things manageable, I'm going to stick to the basics, which will be plenty complex and exciting.</p><a data-type="indexterm" data-primary="genetic algorithms" data-secondary="defined"></a>
|
||
<p>The term “genetic algorithm” refers to a specific algorithm implemented in a specific way to solve specific sorts of problems. While the formal genetic algorithm itself will serve as the foundation for the examples we create in this chapter, I won't try to implement the algorithm with perfect accuracy, given that I am looking for creative uses of evolutionary theories in my code. This chapter will be broken down into the following three parts (with the majority of the time spent on the first).</p><a data-type="indexterm" data-primary="genetic algorithms" data-secondary="traditional"></a><a data-type="indexterm" data-primary="traditional genetic algorithms"></a>
|
||
<ol>
|
||
<li><strong><em>Traditional Genetic Algorithm.</em></strong> I'll begin with the traditional computer science genetic algorithm. This algorithm was developed to solve problems in which the solution space is so vast that a “brute force” algorithm would simply take too long. Here’s an example: I’m thinking of a number. A number between one and one billion. How long will it take for you to guess it? Solving a problem with “brute force” refers to the process of checking every possible solution. Is it one? Is it two? Is it three? Is it four? And so and and so forth. Though luck does play a factor here, with brute force I would wait patiently for years while you count to one billion. However, what if I could tell you if an answer you gave was good or bad? Warm or cold? Very warm? Hot? Super, super cold? If you could evaluate how “fit” a guess is, you could pick other numbers closer to that guess and arrive at the answer more quickly. Your answer could evolve.</li>
|
||
<li><strong><em>Interactive Selection. </em></strong>Once you examine the traditional computer science algorithm, you'll look at other applications of genetic algorithms in the visual arts. Interactive selection refers to the process of evolving something (often an computer-generated image) through user interaction. Let’s say you walk into a museum gallery and see ten paintings. With interactive selection, you would pick your favorites and allow an algorithmic process to generate (or “evolve”) new paintings based on your preferences.<a data-type="indexterm" data-primary="interactive selection genetic algorithms"></a></li>
|
||
<li><strong><em>Ecosystem Simulation.</em></strong> The traditional computer science genetic algorithm and interactive selection technique are what you will likely find if you search online or read a textbook about artificial intelligence. But as you'll soon see, they don’t really simulate the process of evolution as it happens in the real world. In this chapter, I want to also explore techniques for simulating the process of evolution in an ecosystem of pseudo-living beings. How can your objects that move about the screen meet each other, mate, and pass their genes on to a new generation? This would apply directly to the Ecosystem Project outlined at the end of each chapter.</li>
|
||
</ol><a data-type="indexterm" data-primary="ecosystem simulation genetic algorithms"></a><a data-type="indexterm" data-primary="genetic algorithms" data-secondary="ecosystem simulation"></a>
|
||
<h2 id="92-why-use-genetic-algorithms">9.2 Why Use Genetic Algorithms?</h2><a data-type="indexterm" data-primary="<em>Adaptation in Natural and Artificial Systems<" data-secondary="em> (Holland)"></a><a data-type="indexterm" data-primary="evolutionary computing"></a><a data-type="indexterm" data-primary="genetic algorithms" data-secondary="purpose of"></a><a data-type="indexterm" data-primary="Holland" data-secondary="John"></a>
|
||
<p>While computer simulations of evolutionary processes date back to the 1950s, much of what have become commonly referred to as genetic algorithms (also known as “GAs”) today was developed by John Holland, a professor at the University of Michigan, whose book <em>Adaptation in Natural and Artificial Systems</em> pioneered GA research. Today, more genetic algorithms are part of a wider field of research, often referred to as "Evolutionary Computing."</p><a data-type="indexterm" data-primary="brute force method"></a><a data-type="indexterm" data-primary="infinite monkey theorem"></a><a data-type="indexterm" data-primary="probability" data-secondary="infinite monkey theorem"></a>
|
||
<p>To help illustrate the traditional genetic algorithm, I am going to start with monkeys. No, not our evolutionary ancestors. I'm going to start with some fictional monkeys that bang away on keyboards with the goal of typing out the complete works of Shakespeare.</p>
|
||
<figure>
|
||
<img src="images/09_ga/09_ga_1.png" alt="Figure 9.1">
|
||
<figcaption>Figure 9.1</figcaption>
|
||
</figure>
|
||
<p>The “infinite monkey theorem” is stated as follows: A monkey hitting keys randomly on a typewriter will eventually type the complete works of Shakespeare (given an infinite amount of time). The problem with this theory is that the probability of said monkey actually typing Shakespeare is so low that even if that monkey started at the Big Bang, it’s unbelievably unlikely they would even have <em>Hamlet</em> at this point.</p>
|
||
<p>Consider a monkey named George. George types on a reduced typewriter containing only twenty-seven characters: twenty-six letters and one space bar. So the probability of George hitting any given key is one in twenty-seven.</p>
|
||
<p>Next, consider the phrase “to be or not to be that is the question” (I'm simplifying it from the original “To be, or not to be: that is the question”). The phrase is 39 characters long. If George starts typing, the chance he’ll get the first character right is 1 in 27. Since the probability he’ll get the second character right is also 1 in 27, he has a 1 in 27*27 chance of landing the first two characters in correct order—which follows directly from our discussion of "event probability" in the Introduction. Therefore, the probability that George will type the full phrase is:</p>
|
||
<p>(1/27) multiplied by itself 39 times, i.e. (1/27)39</p>
|
||
<p>which equals a 1 in 66,555,937,033,867,822,607,895,549,241,096,482,953,017,615,834,735,226,163 chance of getting it right!</p>
|
||
<p>Needless to say, even hitting just this one phrase, not to mention an entire play, is highly unlikely. Even if George is a computer simulation and can type one million random phrases per second, for George to have a 99% probability of eventually getting it right, he would have to type for 9,719,096,182,010,563,073,125,591,133,903,305,625,605,017 years. (Note that the age of the universe is estimated to be a mere 13,750,000,000 years.)</p>
|
||
<p>The point of all these unfathomably large numbers is not to give you a headache, but to demonstrate that a brute force algorithm (typing every possible random phrase) is not a reasonable strategy for arriving randomly at “to be or not to be that is the question”. Enter genetic algorithms, which will show that you can still start with random phrases and find the solution through simulated evolution.</p>
|
||
<p>Now, it’s worth noting that this problem (<em>arrive at the phrase “to be or not to be that is the question”</em>) is a ridiculous one. Since you know the answer, all you need to do is type it. Here’s a p5.js sketch that solves the problem.</p>
|
||
<pre class="codesplit" data-code-language="javascript">String s = "to be or not to be that is the question";
|
||
print(s);</pre>
|
||
<p>Nevertheless, the point here is that solving a problem with a known answer allows you to easily test your code. Once you've successfully solved the problem, you can feel more confident in using genetic algorithms to do some actual useful work: solving problems with unknown answers. So this first example serves no real purpose other than to demonstrate how genetic algorithms work. If you test the GA results against the known answer and get “to be or not to be”, then you've succeeded in writing a genetic algorithm.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-91">Exercise 9.1</h3>
|
||
<p>Create a sketch that generates random strings. You'll need to know how to do this in order to implement the genetic algorithm example that will shortly follow. How long does it take for p5.js to randomly generate the string “cat”? How could you adapt this to generate a random design using p5.js’s shape-drawing functions?</p>
|
||
</div>
|
||
<h2 id="93-darwinian-natural-selection">9.3 Darwinian Natural Selection</h2><a data-type="indexterm" data-primary="Darwinian natural selection"></a><a data-type="indexterm" data-primary="evolution" data-secondary="Darwinian natural selection"></a><a data-type="indexterm" data-primary="genetic algorithms" data-secondary="Darwinian natural selection"></a><a data-type="indexterm" data-primary="natural phenomena" data-secondary="Darwinian natural selection"></a><a data-type="indexterm" data-primary="natural selection algorithms"></a>
|
||
<p>Before I begin walking through the genetic algorithm, I want to take a moment to describe three core principles of Darwinian evolution that will be required as I implement the simulation. In order for natural selection to occur as it does in nature, all three of these elements must be present.</p><a data-type="indexterm" data-primary="heredity (natural selection)"></a>
|
||
<ol>
|
||
<li><strong><em>Heredity.</em></strong> There must be a process in place by which children receive the properties of their parents. If creatures live long enough to reproduce, then their traits are passed down to their children in the next generation of creatures.</li>
|
||
<li><strong><em>Variation. </em></strong>There must be a variety of traits present in the population or a means with which to introduce variation. For example, imagine there is a population of beetles in which all the beetles are exactly the same: same color, same size, same wingspan, same everything. Without any variety in the population, the children will always be identical to the parents and to each other. New combinations of traits can never occur and nothing can evolve.</li>
|
||
<li><strong><em>Selection. </em></strong>There must be a mechanism by which some members of a population have the opportunity to be parents and pass down their genetic information and some do not. This is typically referred to as “survival of the fittest.” Take for example a population of gazelles is chased by lions every day. The faster gazelles are more likely to escape the lions and are therefore more likely to live longer and have a chance to reproduce and pass their genes down to their children. The term <em>fittest</em>, however, can be a bit misleading. Generally, we think of it as meaning bigger, faster, or stronger. While this may be the case in some instances, natural selection operates on the principle that some traits are better adapted for the creature’s environment and therefore produce a greater likelihood of surviving and reproducing. It has nothing to do with a given creature being “better” (after all, this is a subjective term) or more “physically fit.” In the case of our typing monkeys, for example, a more “fit” monkey is one that has typed a phrase closer to “to be or not to be”.</li>
|
||
</ol>
|
||
<p>Next I’d like to walk through the narrative of the genetic algorithm. I'll do this in the context of the typing monkey. The algorithm itself will be divided into two parts: a set of conditions for initialization (i.e. p5.js’s <code>setup()</code>) and the steps that are repeated over and over again (i.e. p5.js’s <code>draw()</code>) until I arrive at the correct answer.</p>
|
||
<h2 id="94-the-genetic-algorithm-part-i-creating-a-population">9.4 The Genetic Algorithm, Part I: Creating a Population</h2><a data-type="indexterm" data-primary="genetic algorithms" data-secondary="populations" data-tertiary="creating"></a><a data-type="indexterm" data-primary="natural selection algorithms" data-secondary="populations" data-tertiary="creating"></a><a data-type="indexterm" data-primary="populations (genetic algorithms)" data-secondary="creating"></a>
|
||
<p>In the context of the typing monkey example, I will create a population of phrases. (Note that I am using the term “phrase” rather loosely, meaning a string of characters.) This begs the question: How do you create this population? Here is where the Darwinian principle of <strong><em>variation</em></strong> applies. I will say for the sake of simplicity, that I am trying to evolve the phrase “cat” and that I have a population of three phrases.</p>
|
||
<p>ridwon</p>
|
||
<p>Sure, there is variety in the three phrases above, but try to mix and match the characters every which way and you will never get <em>cat</em>. There is not <em>enough</em> variety here to evolve the optimal solution. However, if I had a population of thousands of phrases, all generated randomly, chances are that at least one member of the population will have a <em>c</em> as the first character, one will have an <em>a</em> as the second, and one a <em>t</em> as the third. A large population will most likely give me enough variety to generate the desired phrase (and in Part 2 of the algorithm, I'll have another opportunity to introduce even more variation in case there isn’t enough in the first place). So I can be more specific in describing Step 1 and say:</p>
|
||
<p><span class="highlight">Create a population of randomly generated elements.</span></p><a data-type="indexterm" data-primary="populations (genetic algorithms)" data-secondary="elements of"></a>
|
||
<p>This brings up another important question. What is the element itself? As you move through the examples in this chapter, you'll see several different scenarios; you might have a population of images or a population of vehicles à la Chapter 6. The key, and the part that is new for you in this chapter, is that each member of the population has a virtual “DNA,” a set of properties (you can call them “genes”) that describe how a given element looks or behaves. In the case of the typing monkey, for example, the DNA is simply a string of characters.</p><a data-type="indexterm" data-primary="genotype (natural selection algorithms)"></a><a data-type="indexterm" data-primary="phenotype (natural selection algorithms)"></a>
|
||
<p>In the field of genetics, there is an important distinction between the concepts <em>genotype</em> and <em>phenotype</em>. The actual genetic code—in our case, the digital information itself—is an element’s <strong><em>genotype</em></strong>. This is what gets passed down from generation to generation. The <strong><em>phenotype</em></strong>, however, is the expression of that data. This distinction is key to how you will use genetic algorithms in your own work. What are the objects in your world? How will you design the genotype for your objects (the data structure to store each object’s properties) as well as the phenotype (what are <em>you</em> using these variables to express?) We do this all the time in graphics programming. The simplest example is probably color.</p>
|
||
<p>As you can see, the genotype is the digital information. Each color is a variable that stores an integer and we choose to express that integer as a color. But how you choose to express the data is arbitrary. In a different approach, you could have used the integer to describe the length of a line, the weight of a force, etc.</p>
|
||
<p>The nice thing about our monkey-typing example is that there is no difference between genotype and phenotype. The DNA data itself is a string of characters and the expression of that data is that very string.</p>
|
||
<p>So, we can finally end the discussion of this first step and be more specific with its description, saying:</p>
|
||
<p><span class="highlight">Create a population of N elements, each with randomly generated DNA.</span></p>
|
||
<h2 id="95-the-genetic-algorithm-part-ii-selection">9.5 The Genetic Algorithm, Part II: Selection</h2><a data-type="indexterm" data-primary="genetic algorithms" data-secondary="selection" data-tertiary="implementing"></a><a data-type="indexterm" data-primary="selection (natural selection algorithms)" data-secondary="implementing"></a>
|
||
<p>Here is where we apply the Darwinian principle of <em>selection</em>. We need to evaluate the population and determine which members are fit to be selected as parents for the next generation. The process of selection can be divided into two steps.</p>
|
||
<p><strong><em>1) Evaluate fitness.</em></strong></p><a data-type="indexterm" data-primary="fitness functions (natural selection algorithms)"></a><a data-type="indexterm" data-primary="natural selection algorithms" data-secondary="fitness functions"></a>
|
||
<p>For our genetic algorithm to function properly, we will need to design what is referred to as a <strong><em>fitness function</em></strong>. The function will produce a numeric score to describe the fitness of a given member of the population. This, of course, is not how the real world works at all. Creatures are not given a score; they simply survive or not. But in the case of the traditional genetic algorithm, where we are trying to evolve an optimal solution to a problem, we need to be able to numerically evaluate any given possible solution.</p>
|
||
<p>Let’s examine our current example, the typing monkey. Again, let’s simplify the scenario and say we are attempting to evolve the word “cat”. We have three members of the population: <em>hut</em>, <em>car</em>, and <em>box</em>. <em>Car</em> is obviously the most fit, given that it has two correct characters, <em>hut</em> has only one, and <em>box</em> has zero. And there it is, our fitness function:</p>
|
||
<div data-type="equation">fitness = the number of correct characters</div>
|
||
<p>We will eventually want to look at examples with more sophisticated fitness functions, but this is a good place to start.</p>
|
||
<p><strong><em>2) Create a mating pool.</em></strong></p><a data-type="indexterm" data-primary="mating pools (natural selection)" data-secondary="creating"></a><a data-type="indexterm" data-primary="natural selection algorithms" data-secondary="mating pools" data-tertiary="creating"></a>
|
||
<p>Once the fitness has been calculated for all members of the population, we can then select which members are fit to become parents and place them in a mating pool. There are several different approaches we could take here. For example, we could employ what is known as the <strong><em>elitist</em></strong> method and say, “Which two members of the population scored the highest? You two will make all the children for the next generation.” This is probably one of the easier methods to program; however, it flies in the face of the principle of variation. If two members of the population (out of perhaps thousands) are the only ones available to reproduce, the next generation will have little variety and this may stunt the evolutionary process. We could instead make a mating pool out of a larger number—for example, the top 50% of the population, 500 out of 1,000. This is also just as easy to program, but it will not produce optimal results. In this case, the high-scoring top elements would have the same chance of being selected as a parent as the ones toward the middle. And why should element number 500 have a solid shot of reproducing, while element number 501 has no shot?</p><a data-type="indexterm" data-primary="natural selection algorithms" data-secondary="probability"></a><a data-type="indexterm" data-primary="probability" data-secondary="natural selection algorithms and"></a>
|
||
<p>A better solution for the mating pool is to use a <strong><em>probabilistic</em></strong> method, which we’ll call the “wheel of fortune” (also known as the “roulette wheel”). To illustrate this method, let’s consider a simple example where we have a population of five elements, each with a fitness score.</p><a data-type="indexterm" data-primary="normalization" data-secondary="mating pools" data-tertiary="creating with"></a><a data-type="indexterm" data-primary="roulette wheel probability method"></a><a data-type="indexterm" data-primary="wheel of fortune probability method"></a>
|
||
<p>The first thing we’ll want to do is <strong><em>normalize</em></strong> all the scores. Remember normalizing a vector? That involved taking a vector and standardizing its length, setting it to 1. When we normalize a set of fitness scores, we are standardizing their range to between 0 and 1, as a percentage of total fitness. Let’s add up all the fitness scores.</p>
|
||
<div data-type="equation">total fitness = 3 + 4 + 0.5 + 1.5 + 1 = 10</div>
|
||
<p>Then let’s divide each score by the total fitness, giving us the normalized fitness.</p>
|
||
<p>Now it’s time for the wheel of fortune.</p>
|
||
<figure>
|
||
<img src="images/09_ga/09_ga_2.png" alt="Figure 9.2">
|
||
<figcaption>Figure 9.2</figcaption>
|
||
</figure>
|
||
<p>Spin the wheel and you’ll notice that Element B has the highest chance of being selected, followed by A, then E, then D, and finally C. This probability-based selection according to fitness is an excellent approach. One, it guarantees that the highest-scoring elements will be most likely to reproduce. Two, it does not entirely eliminate any variation from the population. Unlike with the elitist method, even the lowest-scoring element (in this case C) has a chance to pass its information down to the next generation. It’s quite possible (and often the case) that even low-scoring elements have a tiny nugget of genetic code that is truly useful and should not entirely be eliminated from the population. For example, in the case of evolving “to be or not to be”, we might have the following elements.</p>
|
||
<p>As you can see, elements A and B are clearly the most fit and would have the highest score. But neither contains the correct characters for the end of the phrase. Element C, even though it would receive a very low score, happens to have the genetic data for the end of the phrase. And so while we would want A and B to be picked to generate the majority of the next generation, we would still want C to have a small chance to participate in the reproductive process.</p>
|
||
<h2 id="96-the-genetic-algorithm-part-iii-reproduction">9.6 The Genetic Algorithm, Part III: Reproduction</h2><a data-type="indexterm" data-primary="heredity (natural selection)" data-secondary="implementing"></a><a data-type="indexterm" data-primary="natural selection algorithms" data-secondary="reproduction"></a><a data-type="indexterm" data-primary="reproduction (natural selection algorithms)"></a>
|
||
<p>Now that we have a strategy for picking parents, we need to figure out how to use <em>reproduction</em> to make the population’s next generation, keeping in mind the Darwinian principle of heredity—that children inherit properties from their parents. Again, there are a number of different techniques we could employ here. For example, one reasonable (and easy to program) strategy is asexual reproduction, meaning we pick just one parent and create a child that is an exact copy of that parent. The standard approach with genetic algorithms, however, is to pick two parents and create a child according to the following steps.</p>
|
||
<p><strong><em>1) Crossover.</em></strong></p><a data-type="indexterm" data-primary="crossover (natural selection algorithms)"></a><a data-type="indexterm" data-primary="heredity (natural selection)" data-secondary="crossover"></a>
|
||
<p>Crossover involves creating a child out of the genetic code of two parents. In the case of the monkey-typing example, let’s assume we’ve picked two phrases from the mating pool (as outlined in our selection step).</p>
|
||
<p>Parent B: PLAY</p>
|
||
<p>It’s now up to us to make a child phrase from these two. Perhaps the most obvious way (let’s call this the 50/50 method) would be to take the first two characters from A and the second two from B, leaving us with:</p>
|
||
<figure>
|
||
<img src="images/09_ga/09_ga_3.png" alt="Figure 9.3">
|
||
<figcaption>Figure 9.3</figcaption>
|
||
</figure>
|
||
<p>A variation of this technique is to pick a random midpoint. In other words, we don’t have to pick exactly half of the code from each parent. We could sometimes end up with FLAY, and sometimes with FORY. This is preferable to the 50/50 approach, since we increase the variety of possibilities for the next generation.</p>
|
||
<figure>
|
||
<img src="images/09_ga/09_ga_4.png" alt="Figure 9.4: Picking a random midpoint ">
|
||
<figcaption>Figure 9.4: Picking a random midpoint </figcaption>
|
||
</figure>
|
||
<p>Another possibility is to randomly select a parent for each character in the child string. You can think of this as flipping a coin four times: heads take from parent A, tails from parent B. Here we could end up with many different results such as: PLRY, FLRK, FLRY, FORY, etc.</p>
|
||
<figure>
|
||
<img src="images/09_ga/09_ga_5.png" alt="Figure 9.5: Coin-flipping approach ">
|
||
<figcaption>Figure 9.5: Coin-flipping approach </figcaption>
|
||
</figure>
|
||
<p>This strategy will not change how the example behaves from the random midpoint method; however, if the order of the genetic information plays some role in expressing the phenotype, you may prefer one solution over the other.</p>
|
||
<p><strong><em>2) Mutation.</em></strong></p><a data-type="indexterm" data-primary="heredity (natural selection)" data-secondary="mutation"></a><a data-type="indexterm" data-primary="mutation (natural selection algorithms)"></a>
|
||
<p>Once the child DNA has been created via crossover, we apply one final process before adding the child to the next generation—<strong><em>mutation</em></strong>. Mutation is an optional step, as there are some cases in which it is unnecessary. However, it exists because of the Darwinian principle of variation. We created an initial population randomly, making sure that we start with a variety of elements. However, there can only be so much variety when seeding the first generation, and mutation allows us to introduce additional variety throughout the evolutionary process itself.</p>
|
||
<figure class="half-width-right">
|
||
<img src="images/09_ga/09_ga_6.png" alt="Figure 9.6">
|
||
<figcaption>Figure 9.6</figcaption>
|
||
</figure><a data-type="indexterm" data-primary="mutation (natural selection algorithms)" data-secondary="rate of"></a>
|
||
<p>Mutation is described in terms of a <em>rate</em>. A given genetic algorithm might have a mutation rate of 5% or 1% or 0.1%, etc. Let’s assume we just finished with crossover and ended up with the child FORY. If we have a mutation rate of 1%, this means that for each character in the phrase generated from crossover, there is a 1% chance that it will mutate. What does it mean for a character to mutate? In this case, we define mutation as picking a new random character. A 1% probability is fairly low, and most of the time mutation will not occur at all in a four-character string (96% of the time to be more precise). However, when it does, the mutated character is replaced with a randomly generated one (see Figure 9.6).</p>
|
||
<p>As we’ll see in some of the examples, the mutation rate can greatly affect the behavior of the system. Certainly, a very high mutation rate (such as, say, 80%) would negate the evolutionary process itself. If the majority of a child’s genes are generated randomly, then we cannot guarantee that the more “fit” genes occur with greater frequency with each successive generation.</p>
|
||
<p>The process of selection (picking two parents) and reproduction (crossover and mutation) is applied over and over again <code>N</code> times until we have a new population of <code>N</code> elements. At this point, the new population of children becomes the current population and we loop back to evaluate fitness and perform selection and reproduction again.</p>
|
||
<p>Now that we have described all the steps of the genetic algorithm in detail, it’s time to translate these steps into p5.js code. Because the previous description was a bit longwinded, let’s look at an overview of the algorithm first. We’ll then cover each of the three steps in its own section, working out the code.</p>
|
||
<p><strong><em>SETUP:</em></strong></p>
|
||
<p>Step 1: <strong><em>Initialize</em></strong>. Create a population of N elements, each with randomly generated DNA.</p>
|
||
<p><strong><em>LOOP:</em></strong></p>
|
||
<p>Step 2: <strong><em>Selection</em></strong>. Evaluate the fitness of each element of the population and build a mating pool.</p>
|
||
<p>Step 3: <strong><em>Reproduction</em></strong>. Repeat N times:</p>
|
||
<p> a) Pick two parents with probability according to relative fitness. b) Crossover—create a “child” by combining the DNA of these two parents. c) Mutation—mutate the child’s DNA based on a given probability. d) Add the new child to a new population.</p>
|
||
<p>Step 4. Replace the old population with the new population and return to Step 2.</p>
|
||
<h2 id="97-code-for-creating-the-population">9.7 Code for Creating the Population</h2>
|
||
<h3 id="step-1-initialize-population">Step 1: Initialize Population</h3><a data-type="indexterm" data-primary="populations (genetic algorithms)" data-secondary="implementing"></a>
|
||
<p>If I'm going to create a population, I need a data structure to store a list of members of the population. In most cases (such as our typing-monkey example), the number of elements in the population can be fixed, and so we use an array. (Later you'll see examples that involve a growing/shrinking population and I'll use an <code>array</code>.) But an array of what? We need an object that stores the genetic information for a member of the population. I'll call it <strong><em>DNA</em></strong>.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class DNA {
|
||
|
||
}</pre>
|
||
<p>The population will then be an array of <code>DNA</code> objects.</p>
|
||
<pre class="codesplit" data-code-language="javascript">// A population of 100 DNA objects
|
||
let population = [];</pre>
|
||
<p>But what stuff goes in the <code>DNA</code> class? For a typing monkey, its DNA is the random phrase it types, a string of characters. In this particular example, rather than use an actual <code>String</code> object as the genetic code. Instead, we’ll use an array of characters.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class DNA {
|
||
constructor(num){
|
||
//{!1} Each "gene" is one element of the array.
|
||
// We need 18 genes because “to be or not to be” is 18 characters long.
|
||
this.genes = [];
|
||
// fill the genes array with characters
|
||
for (let i = 0; i < num; i++) {
|
||
this.genes[i] = newChar(); // Pick from range of chars
|
||
}
|
||
}
|
||
}</pre>
|
||
<p>By using an array, you'll be able to extend all the code we write into other examples. For example, the DNA of a creature in a physics system might be an array of <code>vector</code><code>s</code>—or for an image, an array of integers (RGB colors). You can describe any set of properties in an array, and even though a string is convenient for this particular sketch, an array will serve as a better foundation for future evolutionary examples.</p>
|
||
<p>My genetic algorithm dictates that I create a population of N elements, each with <em>randomly generated DNA</em>. Therefore, in the object’s constructor, I randomly create each character of the array. Here I write a helper function called <code>newChar()</code> to create a new character.</p>
|
||
<pre class="codesplit" data-code-language="javascript">function newChar() {
|
||
let c = floor(random(63, 122));
|
||
if (c === 63) c = 32;
|
||
if (c === 64) c = 46;
|
||
|
||
return String.fromCharCode(c);
|
||
}
|
||
|
||
class DNA {
|
||
constructor(num) {
|
||
this.genes = [];
|
||
for (let i = 0; i < num; i++) {
|
||
this.genes[i] = newChar(); // Pick from range of chars
|
||
}
|
||
}
|
||
}</pre>
|
||
<p>Now that I have the constructor, I can return to <code>setup()</code> and initialize each <code>DNA</code> object in the population array.</p>
|
||
<pre class="codesplit" data-code-language="javascript">let population = [];
|
||
|
||
function setup() {
|
||
for (let i = 0; i < population.length; i++) {
|
||
//{!1} Initializing each member of the population
|
||
population[i] = new DNA();
|
||
}
|
||
}</pre>
|
||
<p>The <code>DNA</code> class is not at all complete. I'll need to add functions to it to perform all the other tasks in our genetic algorithm, which I'll do as I walk through steps 2 and 3.</p>
|
||
<h3 id="step-2-selection">Step 2: Selection</h3><a data-type="indexterm" data-primary="selection (natural selection algorithms)" data-secondary="implementing"></a>
|
||
<p>Step 2 reads, <em>“Evaluate the fitness of each element of the population and build a mating pool.”</em> I'll start by evaluating each object’s fitness. Earlier I stated that one possible fitness function for the typed phrases is the total number of correct characters. I will revise this fitness function a little bit and state it as the percentage of correct characters—i.e., the total number of correct characters divided by the total characters.</p>
|
||
<div data-type="equation">Fitness = \textrm{Total \# Characters Correct} / \textrm{Total \# Characters}</div>
|
||
<p>Where should I calculate the fitness? Since the <code>DNA</code> class contains the genetic information (the phrase I will test against the target phrase), I can write a function inside the <code>DNA</code> class itself to score its own fitness. Let’s assume I have a target phrase:</p>
|
||
<pre class="codesplit" data-code-language="javascript">let target = "to be or not to be";</pre>
|
||
<p>We can now compare each “gene” against the corresponding character in the target phrase, incrementing a counter each time we get a correct character.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class DNA {
|
||
constructor(num) {
|
||
this.genes = [];
|
||
// We are adding another variable to the
|
||
// DNA class to track fitness.
|
||
this.fitness = 0;
|
||
for (let i = 0; i < num; i++) {
|
||
this.genes[i] = newChar(); // Pick from range of chars
|
||
}
|
||
}
|
||
|
||
|
||
//{!1} Function to score fitness returns floating point % of "correct" characters
|
||
calcFitness(target) {
|
||
let score = 0;
|
||
for (let i = 0; i < this.genes.length; i++) {
|
||
if (this.genes[i] == target.charAt(i)) {
|
||
score++;
|
||
}
|
||
}
|
||
this.fitness = score / target.length;
|
||
}</pre>
|
||
<p>In the sketch's <code>draw()</code>, the very first step I'll take is to call the fitness function for each member of the population.</p>
|
||
<pre class="codesplit" data-code-language="javascript">function draw() {
|
||
|
||
for (let i = 0; i < population.length; i++) {
|
||
population[i].calcFitness();
|
||
}</pre><a data-type="indexterm" data-primary="mating pools (natural selection)" data-secondary="implementing"></a>
|
||
<p>After I have all the fitness scores, I can build the “mating pool” that I'll need for the reproduction step. The mating pool is a data structure from which I'll continuously pick two parents. Recalling my description of the selection process, I want to pick parents with probabilities calculated according to fitness. In other words, the members of the population that have the highest fitness scores should be most likely to be picked; those with the lowest scores, the least likely.</p>
|
||
<p>In the Introduction, I covered the basics of probability and generating a custom distribution of random numbers. I'm going to use those techniques to assign a probability to each member of the population, picking parents by spinning the “wheel of fortune.” Take a look at Figure 9.2 again.</p>
|
||
<figure>
|
||
<img src="images/09_ga/09_ga_7.png" alt="Figure 9.2 (again) ">
|
||
<figcaption>Figure 9.2 (again) </figcaption>
|
||
</figure>
|
||
<p>It might be fun to do something ridiculous and actually program a simulation of a spinning wheel as depicted above. But this is quite unnecessary.</p>
|
||
<figure class="half-width-right">
|
||
<img src="images/09_ga/09_ga_8.png" alt="Figure 9.7">
|
||
<figcaption>Figure 9.7</figcaption>
|
||
</figure>
|
||
<p>Instead you can pick from the five options (ABCDE) according to their probabilities by filling an <code>array</code> with multiple instances of each parent. In other words, let’s say you had a bucket of wooden letters—30 As, 40 Bs, 5 Cs, 15 Ds, and 10 Es.</p>
|
||
<p>If you pick a random letter out of that bucket, there’s a 30% chance you’ll get an A, a 5% chance you’ll get a C, and so on. In the current simulation, that bucket is an <code>array</code>, and each wooden letter is a potential parent. We add each parent to the <code>array</code> N number of times where N is equal to its percentage score.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> //{!1} Start with an empty mating pool.
|
||
let matingPool = [];
|
||
|
||
for (let i = 0; i < population.length; i++) {
|
||
|
||
//{!1} n is equal to fitness times 100,
|
||
// which leaves me with an integer between 0 and 100.
|
||
let n = int(population[i].fitness * 100);
|
||
for (let j = 0; j < n; j++) {
|
||
//{!1} Add each member of the population
|
||
// to the mating pool N times.
|
||
matingPool.push(population[i]);
|
||
}
|
||
}</pre>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-92">Exercise 9.2</h3>
|
||
<p>One of the other methods you can use to generate a custom distribution of random numbers is called the Monte Carlo method. This technique involves picking two random numbers, with the second number acting as a qualifying number and determining if the first random number should be kept or thrown away. Rewrite the above mating pool algorithm to use the Monte Carlo method instead.</p>
|
||
</div>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-93">Exercise 9.3</h3>
|
||
<p>In some cases, the wheel of fortune algorithm will have an extraordinarily high preference for some elements over others. Take the following probabilities:</p>
|
||
<p>This is sometimes undesirable given how it will decrease the amount of variety in this system. A solution to this problem is to replace the calculated fitness scores with the ordinals of scoring (meaning their rank).</p>
|
||
<p>Rewrite the mating pool algorithm to use this method instead.</p>
|
||
</div>
|
||
<h3 id="step-3-reproduction">Step 3: Reproduction</h3><a data-type="indexterm" data-primary="reproduction (natural selection algorithms)" data-secondary="implementing"></a>
|
||
<p>With the mating pool ready to go, it’s time to make some babies. The first step is to pick two parents. Again, it’s somewhat of an arbitrary decision to pick two parents. It certainly mirrors human reproduction and is the standard means in the traditional GA, but in terms of your work, there really aren’t any restrictions here. You could choose to perform “asexual” reproduction with one parent, or come up with a scheme for picking three or four parents from which to generate child DNA. For this code demonstration, I'll stick to two parents and call them <code>parentA</code> and <code>parentB</code>.</p>
|
||
<p>First thing I need are two random indices into the mating pool—random numbers between 0 and the size of the <code>array</code>.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> let a = int(random(matingPool.length);
|
||
let b = int(random(matingPool.length);</pre>
|
||
<p>I can use these indices to retrieve an actual DNA instance from the mating pool.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> let parentA = matingPool[a];
|
||
let parentB = matingPool[b];</pre>
|
||
<p>Because I have multiple instances of the same <code>DNA</code> objects in the mating pool (not to mention that I could pick the same random number twice), it’s possible that <code>parentA</code> and <code>parentB</code> could be the same <code>DNA</code> object. If I wanted to be strict, I could write some code to ensure that I haven’t picked the same parent twice, but I would gain very little efficiency for all that extra code. Still, it’s worth trying this as an exercise.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-94">Exercise 9.4</h3>
|
||
<p>Add code to the above to guarantee that you have picked two unique “parents.”</p>
|
||
</div><a data-type="indexterm" data-primary="crossover (natural selection algorithms)" data-secondary="implementing"></a>
|
||
<p>Once I have the two parents, I can perform <strong><em>crossover</em></strong> to generate the child DNA, followed by <strong><em>mutation</em></strong>.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> // A function for crossover
|
||
let child = parentA.crossover(parentB);
|
||
// A function for mutation
|
||
child.mutate();</pre>
|
||
<p>Of course, the functions <code>crossover()</code> and <code>mutate()</code> don’t magically exist in our <code>DNA</code> class; I will have to write them. The way I called <code>crossover()</code> above indicates that the function receives an instance of DNA as an argument and returns a new instance of DNA, the child.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> // Crossover
|
||
crossover(partner) {
|
||
// A new child
|
||
// The child is a new instance of DNA.
|
||
// Note that the DNA is generated randomly in the constructor,
|
||
// but we will overwrite it below with DNA from parents.
|
||
let child = new DNA(this.genes.length);
|
||
|
||
//{!1} Picking a random “midpoint” in the genes array
|
||
let midpoint = floor(random(this.genes.length));
|
||
|
||
// Half from one, half from the other
|
||
for (let i = 0; i < this.genes.length; i++) {
|
||
if (i > midpoint) child.genes[i] = this.genes[i];
|
||
else child.genes[i] = partner.genes[i];
|
||
}
|
||
return child;
|
||
}</pre>
|
||
<p>The above crossover function uses the “random midpoint” method of crossover, in which the first section of genes is taken from parent A and the second section from parent B.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-95">Exercise 9.5</h3>
|
||
<p>Rewrite the crossover function to use the “coin flipping” method instead, in which each gene has a 50% chance of coming from parent A and a 50% chance of coming from parent B.</p>
|
||
</div><a data-type="indexterm" data-primary="mutation (natural selection algorithms)" data-secondary="implementing"></a>
|
||
<p>The mutate() function is even simpler to write than crossover(). All I need to do is loop through the array of genes and for each randomly pick a new character according to the mutation rate. With a mutation rate of 1%, for example, I would pick a new character one time out of a hundred.</p>
|
||
<pre class="codesplit" data-code-language="javascript">let mutationRate = 0.01;
|
||
|
||
if (random(1) < mutationRate) {
|
||
// Any code here would be executed 1% of the time.
|
||
|
||
}</pre>
|
||
<p>The entire function therefore reads:</p>
|
||
<pre class="codesplit" data-code-language="javascript"> // Based on a mutation probability, picks a new random character
|
||
mutate(mutationRate) {
|
||
//{!1} Looking at each gene in the array
|
||
for (let i = 0; i < this.genes.length; i++) {
|
||
if (random(1) < mutationRate) {
|
||
//{!1} Mutation, a new random character
|
||
this.genes[i] = newChar();
|
||
}
|
||
}
|
||
}</pre>
|
||
<h2 id="98-genetic-algorithms-putting-it-all-together">9.8 Genetic Algorithms: Putting It All Together</h2><a data-type="indexterm" data-primary="genetic algorithms" data-secondary="building"></a>
|
||
<p>You may have noticed that I've essentially walked through the steps of the genetic algorithm twice, once describing it in narrative form and another time with code snippets implementing each of the steps. What I’d like to do in this section is condense the previous two sections into one page, with the algorithm described in just three steps and the corresponding code alongside.</p>
|
||
<div data-type="example">
|
||
<h3 id="example-91-genetic-algorithm-evolving-shakespeare">Example 9.1: Genetic algorithm: Evolving Shakespeare</h3>
|
||
<p>{p5 sketch link}</p>
|
||
<figure>
|
||
<img src="images/09_ga/09_ga_9.png" alt=" ">
|
||
<figcaption> </figcaption>
|
||
</figure>
|
||
</div>
|
||
<pre class="codesplit" data-code-language="javascript">// Variables we need for our GA
|
||
|
||
// Mutation rate
|
||
let mutationRate;
|
||
// Population total
|
||
let totalPopulation = 150;
|
||
|
||
// Population array
|
||
let population = [];
|
||
// Mating pool ArrayList
|
||
let matingPool = [];
|
||
// Target phrase
|
||
let target;
|
||
|
||
function setup() {
|
||
createCanvas(640, 360);
|
||
|
||
//{!2} Initializing target phrase and mutation rate
|
||
target = "to be or not to be";
|
||
mutationRate = 0.01;
|
||
|
||
//{!4}
|
||
for (let i = 0; i < population.length; i++) {
|
||
population[i] = new DNA(target.length);
|
||
}
|
||
}
|
||
|
||
function draw() {
|
||
|
||
//
|
||
|
||
// Step 2a: Calculate fitness.
|
||
for (let i = 0; i < population.length; i++) {
|
||
population[i].calcFitness();
|
||
}
|
||
|
||
// Step 2b: Build mating pool.
|
||
matingPool = [];
|
||
|
||
for (let i = 0; i < population.length; i++) {
|
||
//{!4} Add each member n times according to its fitness score.
|
||
let n = int(population[i].calcFitness * 100);
|
||
for (let j = 0; j < n; j++) {
|
||
matingPool.push(population[i]);
|
||
}
|
||
}
|
||
|
||
//
|
||
for (let i = 0; i < population.length; i++) {
|
||
let a = int(random(matingPool.length));
|
||
let b = int(random(matingPool.length));
|
||
let partnerA = matingPool[a];
|
||
let partnerB = matingPool[b];
|
||
// Step 3a: Crossover
|
||
let child = partnerA.crossover(partnerB);
|
||
// Step 3b: Mutation
|
||
child.mutate(mutationRate);
|
||
|
||
//{!1} Note that we are overwriting the population with the new
|
||
// children. When draw() loops, we will perform all the same
|
||
// steps with the new population of children.
|
||
population[i] = child;
|
||
}
|
||
}</pre>
|
||
<p>The sketch.js file precisely mirrors the steps of the genetic algorithm. However, most of the functionality called upon is actually present in the <code>DNA</code> class itself.</p>
|
||
<pre class="codesplit" data-code-language="javascript">function newChar() {
|
||
let c = floor(random(63, 122));
|
||
if (c === 63) c = 32;
|
||
if (c === 64) c = 46;
|
||
|
||
return String.fromCharCode(c);
|
||
}
|
||
|
||
// Constructor (makes a random DNA)
|
||
class DNA {
|
||
//{.code-wide} Create DNA randomly.
|
||
constructor(num) {
|
||
// The genetic sequence
|
||
this.genes = [];
|
||
this.fitness = 0;
|
||
for (let i = 0; i < num; i++) {
|
||
this.genes[i] = newChar(); // Pick from range of chars
|
||
}
|
||
}
|
||
|
||
//{!3 .code-wide} Converts array to String—PHENOTYPE.
|
||
getPhrase() {
|
||
return this.genes.join("");
|
||
}
|
||
|
||
//{.code-wide} Calculate fitness.
|
||
calcFitness(target) {
|
||
let score = 0;
|
||
for (let i = 0; i < this.genes.length; i++) {
|
||
if (this.genes[i] == target.charAt(i)) {
|
||
score++;
|
||
}
|
||
}
|
||
this.fitness = score / target.length;
|
||
}
|
||
|
||
//{.code-wide} Crossover
|
||
crossover(partner) {
|
||
// A new child
|
||
let child = new DNA(this.genes.length);
|
||
|
||
let midpoint = floor(random(this.genes.length)); // Pick a midpoint
|
||
|
||
// Half from one, half from the other
|
||
for (let i = 0; i < this.genes.length; i++) {
|
||
if (i > midpoint) child.genes[i] = this.genes[i];
|
||
else child.genes[i] = partner.genes[i];
|
||
}
|
||
return child;
|
||
}
|
||
|
||
//{.code-wide} Mutation
|
||
mutate(mutationRate) {
|
||
for (let i = 0; i < this.genes.length; i++) {
|
||
if (random(1) < mutationRate) {
|
||
this.genes[i] = newChar();
|
||
}
|
||
}
|
||
}
|
||
}</pre>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-96">Exercise 9.6</h3>
|
||
<p>Add features to the above example to report more information about the progress of the genetic algorithm itself. For example, show the phrase closest to the target each generation, as well as report on the number of generations, average fitness, etc. Stop the genetic algorithm once it has solved the phrase. Consider writing a <code>Population</code> class to manage the GA, instead of including all the code in draw().</p>
|
||
<figure>
|
||
<img src="images/09_ga/09_ga_10.png" alt=" ">
|
||
<figcaption> </figcaption>
|
||
</figure>
|
||
</div>
|
||
<h2 id="99-genetic-algorithms-make-them-your-own">9.9 Genetic Algorithms: Make Them Your Own</h2><a data-type="indexterm" data-primary="genetic algorithms" data-secondary="modifying"></a>
|
||
<p>The nice thing about using genetic algorithms in a project is that example code can easily be ported from application to application. The core mechanics of selection and reproduction don’t need to change. There are, however, three key components to genetic algorithms that you, the developer, will have to customize for each use. This is crucial to moving beyond trivial demonstrations of evolutionary simulations (as in the Shakespeare example) to creative uses in projects that you make in p5.js and other creative programming environments.</p>
|
||
<h3 id="key-1-varying-the-variables">Key #1: Varying the variables</h3><a data-type="indexterm" data-primary="genetic algorithms" data-secondary="mutation rate" data-tertiary="varying"></a><a data-type="indexterm" data-primary="genetic algorithms" data-secondary="population maximum" data-tertiary="varying"></a>
|
||
<p>There aren’t a lot of variables to the genetic algorithm itself. In fact, if you look at the previous example’s code, you’ll see only two global variables (not including the arrays and <code>array</code><code>s</code> to store the population and mating pool).</p>
|
||
<pre class="codesplit" data-code-language="javascript">let mutationRate = 0.01;
|
||
let totalPopulation = 150;</pre>
|
||
<p>These two variables can greatly affect the behavior of the system, and it’s not such a good idea to arbitrarily assign them values (though tweaking them through trial and error is a perfectly reasonable way to arrive at optimal values).</p>
|
||
<p>The values I chose for the Shakespeare demonstration were picked to virtually guarantee that the genetic algorithm would solve for the phrase, but not too quickly (approximately 1,000 generations on average) so as to demonstrate the process over a reasonable period of time. A much larger population, however, would yield faster results (if the goal were algorithmic efficiency rather than demonstration). Here is a table of some results.</p>
|
||
<p>Notice how increasing the population size drastically reduces the number of generations needed to solve for the phrase. However, it doesn’t necessarily reduce the amount of time. Once our population balloons to fifty thousand elements, the sketch runs slowly, given the amount of time required to process fitness and build a mating pool out of so many elements. (There are, of course, optimizations that could be made should you require such a large population.)</p>
|
||
<p>In addition to the population size, the mutation rate can greatly affect performance.</p>
|
||
<p>Without any mutation at all (0%), you just have to get lucky. If all the correct characters are present somewhere in some member of the initial population, you’ll evolve the phrase very quickly. If not, there is no way for the sketch to ever reach the exact phrase. Run it a few times and you’ll see both instances. In addition, once the mutation rate gets high enough (10%, for example), there is so much randomness involved (1 out of every 10 letters is random in each new child) that the simulation is pretty much back to a random typing monkey. In theory, it will eventually solve the phrase, but you may be waiting much, much longer than is reasonable.</p>
|
||
<h3 id="key-2-the-fitness-function">Key #2: The fitness function</h3><a data-type="indexterm" data-primary="fitness functions (natural selection algorithms)" data-secondary="exponential vs. linear"></a><a data-type="indexterm" data-primary="genetic algorithms" data-secondary="fitness algorithms" data-tertiary="modifying"></a>
|
||
<p>Playing around with the mutation rate or population total is pretty easy and involves little more than typing numbers in your sketch. The real hard work of a developing a genetic algorithm is in writing a fitness function. If you cannot define your problem’s goals and evaluate numerically how well those goals have been achieved, then you will not have successful evolution in your simulation.</p>
|
||
<p>Before I move onto other scenarios exploring other fitness functions, I want to look at flaws in my Shakespearean fitness function. Consider solving for a phrase that is not nineteen characters long, but one thousand. Now, in the case there are two members of the population, one with 800 characters correct and one with 801. Here are their fitness scores:</p>
|
||
<p>There are a couple of problems here. First, I am adding elements to the mating pool N numbers of times, where N equals fitness multiplied by 100. Objects can only be added to an <code>array</code> a whole number of times, and so A and B will both be added 80 times, giving them an equal probability of being selected. Even with an improved solution that takes floating point probabilities into account, 80.1% is only a teeny tiny bit higher than 80%. But getting 801 characters right is a whole lot better than 800 in the evolutionary scenario. I really want to make that additional character count. I want the fitness score for 801 characters to be exponentially better than the score for 800.</p>
|
||
<p>To put it another way, here's a graph the fitness function.</p>
|
||
<figure>
|
||
<img src="images/09_ga/09_ga_11.png" alt="Figure 9.8">
|
||
<figcaption>Figure 9.8</figcaption>
|
||
</figure>
|
||
<p>This is a linear graph; as the number of characters goes up, so does the fitness score. However, what if the fitness increased exponentially as the number of correct characters increased? My graph could then look something like:</p>
|
||
<figure>
|
||
<img src="images/09_ga/09_ga_12.png" alt="Figure 9.9">
|
||
<figcaption>Figure 9.9</figcaption>
|
||
</figure>
|
||
<p>The more correct characters, the even greater the fitness. I can achieve this type of result in a number of different ways. For example, I could say:</p>
|
||
<div data-type="equation">fitness = (number of correct characters) * (number of correct characters)</div>
|
||
<p>Let’s say I have two members of the population, one with five correct characters and one with six. The number 6 is a 20% increase over the number 5. Let’s look at the fitness scores squared.</p>
|
||
<p>The fitness scores increase exponentially relative to the number of correct characters. 36 is a 44% increase over 25.</p>
|
||
<p>Here’s another formula.</p>
|
||
<div data-type="equation">fitness = 2</div>
|
||
<p>Here, the fitness scores increase at a faster rate, doubling with each additional correct character.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-97">Exercise 9.7</h3>
|
||
<p>Rewrite the fitness function to increase exponentially according to the number of correct characters. Note that you will also have to normalize the fitness values to a range between 0 and 1 so they can be added to the mating pool a reasonable number of times.</p>
|
||
</div><a data-type="indexterm" data-primary="fitness functions (natural selection algorithms)" data-secondary="design your own"></a>
|
||
<p>While this rather specific discussion of exponential vs. linear fitness functions is an important detail in the design of a good fitness function, I don’t want you to miss the more important point here: <em>Design your own fitness function!</em> I seriously doubt that any project you undertake in p5.js with genetic algorithms will actually involve counting the correct number of characters in a string. In the context of this book, it’s more likely you will be looking to evolve a creature that is part of a physics system. Perhaps you are looking to optimize the weights of steering behaviors so a creature can best escape a predator or avoid an obstacle or make it through a maze. You have to ask yourself what you’re hoping to evaluate.</p>
|
||
<p>Let’s consider a racing simulation in which a vehicle is evolving a design optimized for speed.</p>
|
||
<div data-type="equation">fitness = total number of frames required for vehicle to reach target</div>
|
||
<p>How about a cannon that is evolving the optimal way to shoot a target?</p>
|
||
<div data-type="equation">fitness = cannonball distance to target</div>
|
||
<p>The design of computer-controlled players in a game is also a common scenario. Let’s say you are programming a soccer game in which the user is the goalie. The rest of the players are controlled by your program and have a set of parameters that determine how they kick a ball towards the goal. What would the fitness score for any given player be?</p>
|
||
<div data-type="equation">fitness = total goals scored</div><a data-type="indexterm" data-primary="fitness functions (natural selection algorithms)" data-secondary="robotic enslavement of humanity and"></a>
|
||
<p>This, obviously, is a simplistic take on the game of soccer, but it illustrates the point. The more goals a player scores, the higher its fitness, and the more likely its genetic information will appear in the next game. Even with a fitness function as simple as the one described here, this scenario is demonstrating something very powerful—the adaptability of a system. If the players continue to evolve from game to game to game, when a new <em>human</em> user enters the game with a completely different strategy, the system will quickly discover that the fitness scores are going down and evolve a new optimal strategy. It will adapt. (Don’t worry, there is very little danger in this resulting in sentient robots that will enslave all humans.)</p>
|
||
<p>In the end, if you do not have a fitness function that effectively evaluates the performance of the individual elements of your population, you will not have any evolution. And the fitness function from one example will likely not apply to a totally different project. So this is the part where you get to shine. You have to design a function, sometimes from scratch, that works for your particular project. And where do you do this? All you have to edit are those few lines of code inside the function that computes the fitness variable.</p>
|
||
<pre class="codesplit" data-code-language="javascript">calcFitness() {
|
||
????????????
|
||
????????????
|
||
this.fitness = ??????????
|
||
}</pre>
|
||
<h3 id="key-3-genotype-and-phenotype">Key #3: Genotype and Phenotype</h3><a data-type="indexterm" data-primary="genotype (natural selection algorithms)" data-secondary="modifying"></a><a data-type="indexterm" data-primary="phenotype (natural selection algorithms)"></a>
|
||
<p>The final key to designing your own genetic algorithm relates to how you choose to encode the properties of your system. What are you trying to express, and how can you translate that expression into a bunch of numbers? What is the genotype and phenotype?</p>
|
||
<p>When talking about the fitness function, I happily assumed I could create computer-controlled kickers that each had a “set of parameters that determine how they kick a ball towards the goal.” However, what those parameters are and how you choose to encode them is up to you.</p>
|
||
<p>I started with the Shakespeare example because of how easy it was to design both the genotype (an array of characters) and its expression, the phenotype (the string drawn in the window).</p>
|
||
<p>The good news is—and I hinted at this at the start of this chapter—you’ve really been doing this all along. Anytime you write a class in p5.js, you make a whole bunch of variables.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class Vehicle {
|
||
constructor(){
|
||
this.maxspeed = ????;
|
||
this.maxforce = ????;
|
||
this.size = ????;
|
||
this.separationWeight = ????;
|
||
[inline]// etc.
|
||
}
|
||
</pre>
|
||
<p>All you need to do to evolve those parameters is to turn them into an array, so that the array can be used with all of the functions—<code>crossover()</code>, <code>mutate()</code>, etc.—found in the <code>DNA</code> class. One common solution is to use an array of floating point numbers between 0 and 1.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class DNA {
|
||
|
||
constructor(num) {
|
||
// An array of floats
|
||
this.genes = [];
|
||
for (let i = 0; i < num; i++) {
|
||
// Always pick a number between 0 and 1.
|
||
this.genes[i] = float(1);
|
||
}
|
||
}</pre>
|
||
<p>Notice how I've now put the genetic data (genotype) and its expression (phenotype) into two separate classes. The <code>DNA</code> class is the genotype and the <code>Vehicle</code> class uses a <code>DNA</code> object to drive its behaviors and express that data visually—it is the phenotype. The two can be linked by creating a <code>DNA</code> instance inside the <code>Vehicle</code> class itself.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class Vehicle {
|
||
constructor() {
|
||
//{!1} A DNA object embedded into the Vehicle class
|
||
this.dna = new DNA(4);
|
||
//{!4} Using the genes to set variables
|
||
this.maxspeed = dna.genes[0];
|
||
this.maxforce = dna.genes[1];
|
||
this.size = dna.genes[2];
|
||
this.separationWeight = dna.genes[3];
|
||
//{!1} Etc.
|
||
}</pre>
|
||
<p>Of course, you most likely don’t want all your variables to have a range between 0 and 1. But rather than try to remember how to adjust those ranges in the <code>DNA</code> class itself, it’s easier to pull the genetic information from the <code>DNA</code> object and use p5.js’s <code>map()</code> function to change the range. For example, if you want a size variable between 10 and 72, you would say:</p>
|
||
<pre class="codesplit" data-code-language="javascript"> this.size = map(this.dna.genes[2], 0, 1, 10, 72);</pre>
|
||
<p>In other cases, you will want to design a genotype that is an array of objects. Consider the design of a rocket with a series of “thruster” engines. You could describe each thruster with a <code>vector</code> that outlines its direction and relative strength.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class DNA {
|
||
constructor(num) {
|
||
// The genotype is an array of vectors.
|
||
this.genes = [];
|
||
for (let i = 0; i < num; i++) {
|
||
//{!1} A PVector pointing in a random direction
|
||
this.genes[i] = p5.Vector.random2D();
|
||
//{!1} And scaled randomly
|
||
this.genes[i].mult(random(10));
|
||
}
|
||
}</pre>
|
||
<p>The phenotype would be a <code>Rocket</code> class that participates in a physics system.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class Rocket {
|
||
constructor(){
|
||
this.dna = ????;
|
||
[inline]// etc.
|
||
}
|
||
</pre><a data-type="indexterm" data-primary="object-oriented programming" data-secondary="genotype" data-tertiary="phenotype objects and"></a>
|
||
<p>What’s great about this technique of dividing the genotype and phenotype into separate classes (<code>DNA</code> and <code>Rocket</code> for example) is that when it comes time to build all of the code, you’ll notice that the <code>DNA</code> class we developed earlier remains intact. The only thing that changes is the array’s data type (<code>float</code>, <code>vector</code>, etc.) and the expression of that data in the phenotype class.</p>
|
||
<p>In the next section, I'll follow this idea a bit further and walk through the necessary steps for an example that involves moving bodies and an array of <code>vector</code><code>s</code> as DNA.</p>
|
||
<h2 id="910-evolving-forces-smart-rockets">9.10 Evolving Forces: Smart Rockets</h2><a data-type="indexterm" data-primary="genetic algorithms" data-secondary="Smart Rockets (Thorp)"></a><a data-type="indexterm" data-primary="Smart Rockets (Thorp)"></a><a data-type="indexterm" data-primary="Thorp" data-secondary="Jer"></a>
|
||
<p>I picked the rocket idea for a specific reason. In 2009, Jer Thorp released a genetic algorithms example on his blog entitled “Smart Rockets.” Jer points out that NASA uses evolutionary computing techniques to solve all sorts of problems, from satellite antenna design to rocket firing patterns. This inspired him to create a Flash demonstration of evolving rockets. Here is a description of the scenario:</p>
|
||
<p>A population of rockets launches from the bottom of the screen with the goal of hitting a target at the top of the screen (with obstacles blocking a straight line path).</p>
|
||
<figure>
|
||
<img src="images/09_ga/09_ga_13.png" alt="Figure 9.10">
|
||
<figcaption>Figure 9.10</figcaption>
|
||
</figure>
|
||
<figure class="half-width-right">
|
||
<img src="images/09_ga/09_ga_14.png" alt="Figure 9.11">
|
||
<figcaption>Figure 9.11</figcaption>
|
||
</figure>
|
||
<p>Each rocket is equipped with five thrusters of variable strength and direction. The thrusters don’t fire all at once and continuously; rather, they fire one at a time in a custom sequence.</p>
|
||
<p>In this section, I'm going to evolve my own simplified Smart Rockets, inspired by Jer Thorp’s. When I get to the end of the section, I'll leave implementing some of Jer’s additional advanced features as an exercise.</p>
|
||
<p>My rockets will have only one thruster, and this thruster will be able to fire in any direction with any strength for every frame of animation. This isn’t particularly realistic, but it will make building out the framework a little easier. (You can always make the rocket and its thrusters more advanced and realistic later.)</p>
|
||
<p>I will start by taking the basic <code>Mover</code> class from Chapter 2 examples and renaming it <code>Rocket</code>.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class Rocket {
|
||
|
||
|
||
constructor(pos){
|
||
// A rocket has three vectors: position, velocity, acceleration.
|
||
this.position = pos.copy();
|
||
this.velocity = createVector();
|
||
this.acceleration = createVector();
|
||
}
|
||
|
||
// Accumulating forces into acceleration (Newton’s 2nd law)
|
||
applyForce(f) {
|
||
this.acceleration.add(f);
|
||
}
|
||
|
||
// Our simple physics model (Euler integration)
|
||
update() {
|
||
// Velocity changes according to acceleration.
|
||
this.velocity.add(this.acceleration);
|
||
//{!2} position changes according to velocity.
|
||
this.position.add(this.velocity);
|
||
this.acceleration.mult(0);
|
||
}
|
||
}</pre>
|
||
<p>Using the above framework, I can implement our smart rocket by saying that for every frame of animation, I call <code>applyForce()</code> with a new force. The “thruster” applies a single force to the rocket each time through <code>draw()</code>.</p>
|
||
<p>Considering this example, I will go through the three keys to programming a custom genetic algorithm example as outlined in the previous section.</p>
|
||
<p><strong>Key #1: Population size and mutation rate</strong></p>
|
||
<p>I will actually hold off on this first key for the moment. My strategy will be to pick some reasonable numbers (a population of 100 rockets, mutation rate of 1%) and build out the system, playing with these numbers once I have my sketch up and running.</p>
|
||
<p><strong>Key #2: The fitness function</strong></p>
|
||
<p>I stated the goal of a rocket reaching a target. In other words, the closer a rocket gets to the target, the higher the fitness. Fitness is inversely proportional to distance: the smaller the distance, the greater the fitness; the greater the distance, the smaller the fitness.</p>
|
||
<p>Let’s assume I have a <code>vector</code> target.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> calcfitness() {
|
||
// How close did we get?
|
||
let d = p5.Vector.dist(this.position, target);
|
||
//{!1} Fitness is inversely proportional to distance.
|
||
this.fitness = 1/d;
|
||
}</pre>
|
||
<p>This is perhaps the simplest fitness function I could write. By using one divided by distance, large distances become small numbers and small distances become large.</p>
|
||
<p>And if I wanted to use my exponential trick from the previous section, I could use one divided by distance squared.</p>
|
||
<p>There are several additional improvements I'll want to make to the fitness function, but this simple one is a good start.</p>
|
||
<pre class="codesplit" data-code-language="javascript">calcFitness() {
|
||
let d = p5.Vector.dist(position, target);
|
||
//{!1} Squaring 1 divided by distance
|
||
this.fitness = pow(1/d, 2);
|
||
}</pre>
|
||
<p><span class="highlight">Key #3: Genotype and Phenotype</span></p>
|
||
<p>I stated that each rocket has a thruster that fires in a variable direction with a variable magnitude in each frame. And so I need a <code>vector</code> for each frame of animation. The genotype, the data required to encode the rocket’s behavior, is therefore an array of <code>vector</code><code>s</code>.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class DNA {
|
||
constructor(num){
|
||
this.genes = [];
|
||
}</pre>
|
||
<p>The happy news here is that I don’t really have to do anything else to the <code>DNA</code> class. All of the functionality we developed for the typing monkey (crossover and mutation) applies here. The one difference I do have to consider is how to initialize the array of genes. With the typing monkey, I had an array of characters and picked a random character for each element of the array. Here I'll do exactly the same thing and initialize a DNA sequence as an array of random <code>vector</code><code>s</code>. Now, your instinct in creating a random <code>vector</code> might be as follows:</p>
|
||
<pre class="codesplit" data-code-language="javascript">let v = createVector(random(-1, 1), random(-1, 1));</pre>
|
||
<figure class="half-width-right">
|
||
<img src="images/09_ga/09_ga_15.png" alt="Figure 9.12">
|
||
<figcaption>Figure 9.12</figcaption>
|
||
</figure>
|
||
<p>This is perfectly fine and will likely do the trick. However, if I were to draw every single possible vector I might pick, the result would fill a square (see Figure 9.12). In this case, it probably doesn’t matter, but there is a slight bias to diagonals here given that a <code>vector</code> from the center of a square to a corner is longer than a purely vertical or horizontal one.</p>
|
||
<figure class="half-width-right">
|
||
<img src="images/09_ga/09_ga_16.png" alt="Figure 9.13">
|
||
<figcaption>Figure 9.13</figcaption>
|
||
</figure>
|
||
<p>What would be better here is to pick a random angle and make a <code>vector</code> of length one from that angle, giving us a circle (see Figure 9.13). This could be easily done with a quick polar to Cartesian conversion, but a quicker path to the result is just to use <code>vector</code>'s <code>random2D()</code>.</p>
|
||
<pre class="codesplit" data-code-language="javascript">for (let i = 0; i < num; i++) {
|
||
//{!1} Making a PVector from a random angle
|
||
this.genes[i] = p5.Vector.random2D();
|
||
}</pre>
|
||
<p>A <code>vector</code> of length one is actually going to be quite a large force. Remember, forces are applied to acceleration, which accumulates into velocity thirty times per second. So, for this example, I will also add one more variable to the <code>DNA</code> class: a maximum force that scales all the <code>vector</code><code>s</code>. This will control the thruster power.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class DNA {
|
||
constructor() {
|
||
// We need a PVector for every frame of the rocket’s life.
|
||
// The genetic sequence is an array of vectors.
|
||
this.genes = [];
|
||
// How strong can the thrusters be?
|
||
this.maxForce = 0.1;
|
||
// notice that genes are equal to a global variable called lifetime
|
||
for (let i = 0; i < lifetime; i++) {
|
||
this.genes[i] = p5.Vector.random2D();
|
||
//{!1} Scaling the vectors randomly,
|
||
// but no stronger than maximum force
|
||
this.genes[i].mult(random(0, maxforce));
|
||
}
|
||
}</pre>
|
||
<p>Notice also that I created an array of <code>vector</code><code>s</code> with length lifetime. I need a <code>vector</code> for each frame of the rocket’s life, and the above assumes the existence of a global variable lifetime that stores the total number of frames in each generation’s life cycle.</p>
|
||
<p>The expression of this array of <code>vector</code><code>s</code>, the phenotype, is a <code>Rocket</code> class modeled on our basic <code>vector</code> and forces examples from Chapter 2. All I need to do is add an instance of a <code>DNA</code> object to the class. The fitness variable will also live here. Only the <code>Rocket</code> object knows how to compute its distance to the target, and therefore the fitness function will live here in the phenotype as well.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class Rocket {
|
||
constructor(pos, dna){
|
||
// A Rocket has DNA.
|
||
this.dna = dna;
|
||
// A Rocket has fitness.
|
||
this.fitness = 0;
|
||
|
||
this.position = pos.copy();
|
||
this.velocity = createVector();
|
||
this.acceleration = createVector();
|
||
}
|
||
</pre>
|
||
<p>What am I using the DNA for? Here I am marching through the array of <code>vector</code><code>s</code> and applying them one at a time as a force to the rocket. To do this, I'll also have to add an integer that acts as a counter to walk through the array.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class Rocket {
|
||
constructor(pos, dna){
|
||
// A Rocket has DNA.
|
||
this.dna = dna;
|
||
// A Rocket has fitness.
|
||
this.fitness = 0;
|
||
this.geneCounter = 0;
|
||
this.position = pos.copy();
|
||
this.velocity = createVector();
|
||
this.acceleration = createVector();
|
||
}
|
||
|
||
|
||
run() {
|
||
// Apply a force from the genes array.
|
||
this.applyForce(this.dna.genes[this.geneCounter]);
|
||
// Go to the next force in the genes array.
|
||
this.geneCounter++;
|
||
//{!1} Update the Rocket’s physics.
|
||
this.update();
|
||
}</pre>
|
||
<h2 id="911-smart-rockets-putting-it-all-together">9.11 Smart Rockets: Putting It All Together</h2>
|
||
<p>Now I have a <code>DNA</code> class (genotype) and a <code>Rocket</code> class (phenotype). The last piece of the puzzle is a <code>Population</code> class, which manages an array of rockets and has the functionality for selection and reproduction. Again, the happy news here is that I barely have to change anything from the Shakespeare monkey example. The process for building a mating pool and generating a new array of child rockets is exactly the same as what I did with our population of strings.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class Population {
|
||
// Population has variables to keep
|
||
// track of mutation rate, current
|
||
// population array, mating pool, and
|
||
// number of generations.
|
||
constructor(m, num) {
|
||
this.mutationRate = m; // Mutation rate
|
||
this.population = new Array(num); // Array to hold the current population
|
||
this.matingPool = []; // ArrayList which we will use for our "mating pool"
|
||
this.generations = 0; // Number of generations
|
||
//make a new set of creatures
|
||
for (let i = 0; i < this.population.length; i++) {
|
||
let position = createVector(width / 2, height + 20);
|
||
this.population[i] = new Rocket(position, new DNA(), this.population.length);
|
||
}
|
||
}
|
||
|
||
// These functions haven’t changed, so
|
||
// no need to go through the code again.
|
||
calcFitness() {}
|
||
selection() {}
|
||
reproduction() {}</pre>
|
||
<p>There is one fairly significant change, however. With typing monkeys, a random phrase was evaluated as soon as it was created. The string of characters had no lifespan; it existed purely for the purpose of calculating its fitness and then I moved on. The rockets, however, need to live for a period of time before they can be evaluated; they need to be given a chance to make their attempt at reaching the target. Therefore, I need to add one more function to the <code>Population</code> class that runs the physics simulation itself. This is identical to what I did in the <code>run()</code> function of a particle system—update all the particle positions and draw them.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> live () {
|
||
for (let i = 0; i < this.population.length; i++) {
|
||
//{!1} The run function takes care of
|
||
// the forces, updating the rocket’s
|
||
// position, and displaying it.
|
||
this.population[i].run();
|
||
}
|
||
}</pre>
|
||
<p>Finally, I'm ready for <code>setup()</code> and <code>draw()</code>. Here in the main tab, my primary responsibility is to implement the steps of the genetic algorithm in the appropriate order by calling the functions in the <code>Population</code> class.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> population.fitness();
|
||
population.selection();
|
||
population.reproduction();</pre>
|
||
<p>However, unlike the Shakespeare example, I don’t want to do this every frame. Rather, my steps work as follows:</p>
|
||
<ol>
|
||
<li>Create a population of rockets</li>
|
||
<li>Let the rockets live for N frames</li>
|
||
<li>Evolve the next generation
|
||
<ul>
|
||
<li>Selection</li>
|
||
<li>Reproduction</li>
|
||
</ul>
|
||
</li>
|
||
<li>Return to Step #2</li>
|
||
</ol>
|
||
<div data-type="example">
|
||
<h3 id="example-92-simple-smart-rockets">Example 9.2: Simple Smart Rockets</h3>
|
||
<p>https://editor.p5js.org/embed/S1PaKpQOe</p>
|
||
<figure>
|
||
<img src="images/09_ga/09_ga_17.png" alt=" ">
|
||
<figcaption> </figcaption>
|
||
</figure>
|
||
</div>
|
||
<pre class="codesplit" data-code-language="javascript">// How many frames does a generation live for?
|
||
let lifetime;
|
||
|
||
// What frame are we on?
|
||
let lifeCounter;
|
||
|
||
// The population
|
||
let population;
|
||
|
||
function setup() {
|
||
createCanvas(640, 480);
|
||
lifetime = 500;
|
||
lifeCounter = 0;
|
||
|
||
let mutationRate = 0.01;
|
||
//{!1} Step 1: Create the population.
|
||
// Here is where we could play with
|
||
// the mutation rate and population size.
|
||
population = new Population(mutationRate, 50);
|
||
}
|
||
|
||
function draw() {
|
||
background(255);
|
||
// The revised genetic algorithm
|
||
if (lifeCounter < lifetime) {
|
||
// Step 2: The rockets live their
|
||
// life until lifeCounter reaches lifetime.
|
||
population.live();
|
||
lifeCounter++;
|
||
} else {
|
||
// When lifetime is reached, reset
|
||
// lifeCounter and evolve the next
|
||
// generation (Steps 3 and 4,
|
||
// selection and reproduction).
|
||
lifeCounter = 0;
|
||
population.fitness();
|
||
population.selection();
|
||
population.reproduction();
|
||
}
|
||
}</pre>
|
||
<p>The above example works, but it isn’t particularly interesting. After all, the rockets simply evolve to having DNA with a bunch of vectors that point straight upwards. In the next example, I'm going to talk through two suggested improvements for the example and provide code snippets that implement these improvements.</p>
|
||
<figure>
|
||
<img src="images/09_ga/09_ga_18.png" alt=" ">
|
||
<figcaption> </figcaption>
|
||
</figure>
|
||
<p><span class="highlight">Improvement #1: Obstacles</span></p><a data-type="indexterm" data-primary="fitness functions (natural selection algorithms)" data-secondary="avoidance of obstacles and"></a>
|
||
<p>Adding obstacles that the rockets must avoid will make the system more complex and demonstrate the power of the evolutionary algorithm more effectively. I can make rectangular, stationary obstacles fairly easily by creating a class that stores a position and dimensions.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class Obstacle {
|
||
constructor(x, y, w, h) {
|
||
this.position = createVector(x, y);
|
||
this.w = w;
|
||
this.h = h;
|
||
}</pre>
|
||
<p>I can also write a <code>contains()</code> function that will <code>return true</code> or <code>return false</code> to determine if a rocket has hit the obstacle.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> contains(spot) {
|
||
if (spot.x > this.position.x && spot.x < this.position.x + this.w &&
|
||
spot.y > this.position.y && spot.y < this.position.y + this.h) {
|
||
return true;
|
||
} else {
|
||
return false;
|
||
}
|
||
}</pre>
|
||
<p>Assuming I make an <code>array</code> of obstacles, I can then have each rocket check to see if it has collided with an obstacle and set a <code>boolean</code> flag to be true if it does, adding a function to the rocket class.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> // This new function lives in the rocket
|
||
// class and checks if a rocket has
|
||
// hit an obstacle.
|
||
obstacles(os) {
|
||
for (let i = 0; i < os.length; i++) {
|
||
let obs = os[i];
|
||
if (obs.contains(this.position)) {
|
||
this.hitObstacle = true;
|
||
}
|
||
}
|
||
}</pre>
|
||
<p>If the rocket hits an obstacle, I choose to stop it from updating its position.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> run(os) {
|
||
if (!this.hitObstacle && !this.hitTarget) {
|
||
this.applyForce(this.dna.genes[this.geneCounter]);
|
||
this.geneCounter = (this.geneCounter + 1) % this.dna.genes.length;
|
||
this.update();
|
||
// If I hit an edge or an obstacle
|
||
this.obstacles(os);
|
||
}
|
||
// Draw me!
|
||
if (!this.hitObstacle) {
|
||
this.display();
|
||
}
|
||
}</pre>
|
||
<p>And we also have an opportunity to adjust the rocket’s fitness. We consider it to be pretty terrible if the rocket hits an obstacle, and so its fitness should be greatly reduced.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> calcFitness() {
|
||
let d = dist(this.position.x, this.position.y, target.position.x, target.position.y);
|
||
fitness = pow(1/d, 2);
|
||
//{.bold !1}
|
||
if (stopped) fitness *= 0.1;
|
||
}</pre>
|
||
<p><span class="highlight">Improvement #2: Evolve reaching the target faster</span></p><a data-type="indexterm" data-primary="fitness functions (natural selection algorithms)" data-secondary="evolving for specific attributes"></a>
|
||
<p>If you look closely at the first Smart Rockets example, you’ll notice that the rockets are not rewarded for getting to the target faster. The only variable in their fitness calculation is the distance to the target at the end of the generation’s life. In fact, in the event that the rockets get very close to the target but overshoot it and fly past, they may actually be penalized for getting to the target faster. Slow and steady wins the race in this case.</p>
|
||
<p>I could improve the algorithm to optimize for speed a number of ways. First, instead of using the distance to the target at the end of the generation, I could use the distance that is the closest to the target at any point during the rocket’s life. I would call this the rocket’s “record” distance. (All of the code snippets in this section live inside the <code>Rocket</code> class.)</p>
|
||
<pre class="codesplit" data-code-language="javascript">
|
||
checkTarget() {
|
||
let d = dist(this.position.x, this.position.y, target.position.x, target.position.y);
|
||
// Every frame, we check its distance and see
|
||
// if it’s closer than the “record” distance.
|
||
// If it is, we have a new record.
|
||
if (d < this.recordDist) this.recordDist = d;
|
||
|
||
</pre>
|
||
<p>In addition, a rocket should be rewarded according to how quickly it reaches the target. The faster it reaches the target, the higher the fitness. The slower, the lower. To accomplish this, we can increment a counter every cycle of the rocket’s life until it reaches the target. At the end of its life, the counter will equal the amount of time the rocket took to reach that target.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> // If the object reaches the target,
|
||
// set a boolean flag to true.
|
||
if (target.contains(this.position) && !this.hitTarget) {
|
||
this.hitTarget = true;
|
||
} else if (!this.hitTarget) {
|
||
this.finishTime++;
|
||
}
|
||
}</pre>
|
||
<p>Fitness is also inversely proportional to <code>finishTime</code>, and so I can improve the fitness function as follows:</p>
|
||
<pre class="codesplit" data-code-language="javascript"> calcFitness() {
|
||
if (this.recordDist < 1) this.recordDist = 1;
|
||
|
||
// Reward finishing faster and getting close
|
||
this.fitness = (1 / (this.finishTime * this.recordDist));
|
||
|
||
// Make the function exponential
|
||
this.fitness = pow(this.fitness, 4);
|
||
|
||
if (this.hitObstacle) this.fitness *= 0.1; // lose 90% of fitness hitting an obstacle
|
||
if (this.hitTarget) this.fitness *= 2; // twice the fitness for finishing!
|
||
}</pre>
|
||
<p>These improvements are both incorporated into the code for Example 9.3: Smart Rockets.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-98">Exercise 9.8</h3>
|
||
<p>Create a more complex obstacle course. As you make it more difficult for the rockets to reach the target, do you need to improve other aspects of the GA—for example, the fitness function?</p>
|
||
</div>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-99">Exercise 9.9</h3>
|
||
<p>Implement the rocket firing pattern of Jer Thorp’s Smart Rockets. Each rocket only gets five thrusters (of any direction and strength) that follow a firing sequence (of arbitrary length). Jer’s simulation also gives the rockets a finite amount of fuel.</p>
|
||
</div>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-910">Exercise 9.10</h3>
|
||
<p>Visualize the rockets differently. Can you draw a line for the shortest path to the target? Can you add particle systems that act as smoke in the direction of the rocket thrusters?</p>
|
||
</div>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-911">Exercise 9.11</h3>
|
||
<p>Another way to achieve a similar result is to evolve a flow field. Can you make the genotype of a rocket a flow field of <code>vector</code><code>s</code>?</p>
|
||
</div><a data-type="indexterm" data-primary="Evolved Virtual Creatures (Sims)"></a><a data-type="indexterm" data-primary="Sims" data-secondary="Karl"></a>
|
||
<p>One of the more famous implementations of genetic algorithms in computer graphics is Karl Sims’s “Evolved Virtual Creatures.” In Sims’s work, a population of digital creatures (in a simulated physics environment) is evaluated for the creatures' ability to perform tasks, such as swimming, running, jumping, following, and competing for a green cube.</p>
|
||
<p>One of the innovations in Sims’s work is a node-based genotype. In other words, the creature’s DNA is not a linear list of <code>vector</code><code>s</code> or numbers, but a map of nodes. (For an example of this, take a look at Exercise 5.15, toxiclibs' Force Directed Graph.) The phenotype is the creature’s design itself, a network of limbs connected with muscles.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-912">Exercise 9.12</h3>
|
||
<figure class="half-width-right">
|
||
<img src="images/09_ga/09_ga_19.png" alt=" ">
|
||
<figcaption> </figcaption>
|
||
</figure>
|
||
<p>Using toxiclibs or Box2D as the physics model, can you create a simplified 2D version of Sims’s creatures? For a lengthier description of Sims’s techniques, I suggest you watch the video and read Sims’s paper Virtual Creatures. In addition, you can find a similar example that uses Box2D to evolve a “car”: BoxCar2D.</p>
|
||
<figure>
|
||
<img src="images/09_ga/09_ga_20.png" alt=" ">
|
||
<figcaption> </figcaption>
|
||
</figure>
|
||
</div>
|
||
<h2 id="912-interactive-selection">9.12 Interactive Selection</h2><a data-type="indexterm" data-primary="interactive selection genetic algorithms"></a>
|
||
<p>In addition to Evolved Virtual Creatures, Sims is also well known for his museum installation <em>Galapagos</em>. Originally installed in the Intercommunication Center in Tokyo in 1997, the installation consists of twelve monitors displaying computer-generated images. These images evolve over time, following the genetic algorithm steps of selection and reproduction. The innovation here is not the use of the genetic algorithm itself, but rather the strategy behind the fitness function. In front of each monitor is a sensor on the floor that can detect the presence of a user viewing the screen. The fitness of an image is tied to the length of time that viewers look at the image. This is known as <em>interactive selection</em>, a genetic algorithm with fitness values assigned by users.</p>
|
||
<p>Think of all the rating systems you’ve ever used. Could you evolve the perfect movie by scoring all films according to your Netflix ratings? The perfect singer according to American Idol voting?</p>
|
||
<figure class="half-width-right">
|
||
<img src="images/09_ga/09_ga_21.png" alt="Figure 9.14">
|
||
<figcaption>Figure 9.14</figcaption>
|
||
</figure>
|
||
<p>To illustrate this technique, I'm going to build a population of simple faces. Each face will have a set of properties: head size, head color, eye position, eye size, mouth color, mouth position, mouth width, and mouth height.</p>
|
||
<p>The face’s DNA (genotype) is an array of floating point numbers between 0 and 1, with a single value for each property.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class DNA {
|
||
constructor(newgenes) {
|
||
// DNA is random floating point values between 0 and 1 (!!)
|
||
// The genetic sequence
|
||
let len = 20; // Arbitrary length
|
||
if (newgenes) {
|
||
this.genes = newgenes;
|
||
} else {
|
||
this.genes = new Array(len);
|
||
for (let i = 0; i < this.genes.length; i++) {
|
||
this.genes[i] = random(0, 1);
|
||
}
|
||
}
|
||
}</pre>
|
||
<p>The phenotype is a <code>Face</code> class that includes an instance of a <code>DNA</code> object.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class Face {
|
||
constructor(dna){
|
||
this.dna = dna; // Face's DNA
|
||
this.fitness = 1; // How good is this face?
|
||
}
|
||
</pre>
|
||
<p>When it comes time to draw the face on screen, I will use p5.js’s <code>map()</code> function to convert any gene value to the appropriate range for pixel dimensions or color values. (In this case, we are also using <code>colorMode()</code> to set the RGB ranges between 0 and 1.)</p>
|
||
<pre class="codesplit" data-code-language="javascript"> display() {
|
||
|
||
//{.offset-top} Using map() to convert the genes to a range for drawing the face.
|
||
// We are using the face's DNA to pick properties for this face
|
||
// such as: head size, color, eye position, etc.
|
||
let genes = this.dna.genes;
|
||
let r = map(genes[0], 0, 1, 0, 70);
|
||
let c = color(genes[1], genes[2], genes[3]);
|
||
let eye_y = map(genes[4], 0, 1, 0, 5);
|
||
let eye_x = map(genes[5], 0, 1, 0, 10);
|
||
let eye_size = map(genes[5], 0, 1, 0, 10);
|
||
let eyecolor = color(genes[4], genes[5], genes[6]);
|
||
let mouthColor = color(genes[7], genes[8], genes[9]);
|
||
let mouth_y = map(genes[5], 0, 1, 0, 25);
|
||
let mouth_x = map(genes[5], 0, 1, -25, 25);
|
||
let mouthw = map(genes[5], 0, 1, 0, 50);
|
||
let mouthh = map(genes[5], 0, 1, 0, 10);</pre>
|
||
<p>So far, I'm not really doing anything new. This is what I've done in every GA example so far. What’s new is that I'm are not going to write a <code>fitness()</code> function in which the score is computed based on a math formula. Instead, I am going to ask the user to assign the fitness.</p><a data-type="indexterm" data-primary="interactive selection genetic algorithms" data-secondary="user interaction and"></a>
|
||
<p>Now, how best to ask a user to assign fitness is really more of an interaction design problem, and it isn’t really within the scope of this book. So I'm not going to launch into an elaborate discussion of how to program sliders or build your own hardware dials or build a Web app for users to submit online scores. How you choose to acquire fitness scores is really up to you and the particular application you are developing.</p>
|
||
<p>For this simple demonstration, I'll increase fitness whenever a user rolls the mouse over a face. The next generation is created when the user presses a button with an “evolve next generation” label.</p>
|
||
<p>Look at how the steps of the genetic algorithm are applied in the sketch.js file, noting how fitness is assigned according to mouse interaction and the next generation is created on a button press. The rest of the code for checking mouse positions, button interactions, etc. can be found in the accompanying example code.</p>
|
||
<div data-type="example">
|
||
<h3 id="example-94-interactive-selection">Example 9.4: Interactive selection</h3>
|
||
<p>https://editor.p5js.org/embed/SyCZs6m_e</p>
|
||
<figure>
|
||
<img src="images/09_ga/09_ga_22.png" alt=" ">
|
||
<figcaption> </figcaption>
|
||
</figure>
|
||
</div>
|
||
<pre class="codesplit" data-code-language="javascript">let population;
|
||
let info;
|
||
|
||
function setup() {
|
||
createCanvas(800, 124);
|
||
colorMode(RGB, 1.0, 1.0, 1.0, 1.0);
|
||
let popmax = 10;
|
||
let mutationRate = 0.05; // A pretty high mutation rate here, our population is rather small we need to enforce variety
|
||
// Create a population with a target phrase, mutation rate, and population max
|
||
population = new Population(mutationRate, popmax);
|
||
// A simple button class
|
||
button = createButton("evolve new generation");
|
||
button.mousePressed(nextGen);
|
||
button.position(10, 140);
|
||
info = createDiv('');
|
||
info.position(10, 175);
|
||
}
|
||
|
||
function draw() {
|
||
background(1);
|
||
// Display the faces
|
||
population.display();
|
||
population.rollover(mouseX, mouseY);
|
||
info.html("Generation #:" + population.getGenerations());
|
||
}
|
||
|
||
// If the button is clicked, evolve next generation
|
||
function nextGen() {
|
||
population.selection();
|
||
population.reproduction();
|
||
}</pre>
|
||
<p>This example, it should be noted, is really just a demonstration of the idea of interactive selection and does not achieve a particularly meaningful result. For one, I didn’t take much care in the visual design of the faces; they are just a few simple shapes with sizes and colors. Sims, for example, used more elaborate mathematical functions as his images’ genotype. You might also consider a vector-based approach, in which a design’s genotype is a set of points and/or paths.</p><a data-type="indexterm" data-primary="interactive selection genetic algorithms" data-secondary="time lag and"></a>
|
||
<p>The more significant problem here, however, is one of time. In the natural world, evolution occurs over millions of years. In the computer simulation world in the previous examples, the populations are able to evolve behaviors relatively quickly because you are producing new generations algorithmically. In the Shakespeare monkey example, a new generation was born in each frame of animation (approximately sixty per second). Since the fitness values were computed according to a math formula, you could also have had arbitrarily large populations that increased the speed of evolution. In the case of interactive selection, however, you have to sit and wait for a user to rate each and every member of the population before you can get to the next generation. A large population would be unreasonably tedious to deal with—not to mention, how many generations could you stand to sit through?</p>
|
||
<p>There are certainly clever solutions around this. Sims’s Galapagos exhibit concealed the rating process from the users, as it occurred through the normal behavior of looking at artwork in a museum setting. Building a Web application that would allow many users to rate a population in a distributed fashion is also a good strategy for achieving many ratings for large populations quickly.</p>
|
||
<p>In the end, the key to a successful interactive selection system boils down to the same keys we previously established. What is the genotype and phenotype? And how do you calculate fitness, which in this case we can revise to say: “What is your strategy for assigning fitness according to user interaction?”</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-914">Exercise 9.14</h3>
|
||
<p>Build your own interactive selection project. In addition to a visual design, consider evolving sounds—for example, a short sequence of tones. Can you devise a strategy, such as a Web application or physical sensor system, to acquire ratings from many users over time?</p>
|
||
</div>
|
||
<h2 id="913-ecosystem-simulation">9.13 Ecosystem Simulation</h2><a data-type="indexterm" data-primary="ecosystem simulation genetic algorithms"></a><a data-type="indexterm" data-primary="genetic algorithms" data-secondary="ecosystem simulation"></a><a data-type="indexterm" data-primary="natural phenomena" data-secondary="ecosystems" data-tertiary="modeling"></a><a data-type="indexterm" data-primary="populations (genetic algorithms)" data-secondary="ecosystem simulations and"></a>
|
||
<p>You may have noticed something a bit odd about every single evolutionary system you've built so far in this chapter. After all, in the real world, a population of babies isn’t born all at the same time. Those babies don’t then grow up and all reproduce at exactly the same time, then instantly die to leave the population size perfectly stable. That would be ridiculous. Not to mention the fact that there is certainly no one running around the forest with a calculator crunching numbers and assigning fitness values to all the creatures.</p>
|
||
<p>In the real world, you don’t really have “survival of the fittest”; you have “survival of the survivors.” Things that happen to live longer, for whatever reason, have a greater chance of reproducing. Babies are born, they live for a while, maybe they themselves have babies, maybe they don’t, and then they die.</p>
|
||
<p>You won’t necessarily find simulations of “real-world” evolution in artificial intelligence textbooks. Genetic algorithms are generally used in the more formal manner we outlined in this chapter. However, since you are reading this book to develop simulations of natural systems, it’s worth looking at some ways in which you might use a genetic algorithm to build something that resembles a living “ecosystem,” much like the one I've described in the exercises at the end of each chapter.</p>
|
||
<p>I'll begin by developing a very simple scenario. I'll create a creature called a "bloop," a circle that moves about the screen according to Perlin noise. The creature will have a radius and a maximum speed. The bigger it is, the slower it moves; the smaller, the faster.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class Bloop {
|
||
constructor(l, dna) {
|
||
this.position = l.copy(); // Location
|
||
this.xoff = random(1000); // For perlin noise
|
||
this.yoff = random(1000);
|
||
this.dna = dna; // DNA
|
||
// DNA will determine size and maxspeed
|
||
// The bigger the bloop, the slower it is
|
||
this.maxspeed = map(this.dna.genes[0], 0, 1, 15, 0);
|
||
this.r = map(this.dna.genes[0], 0, 1, 0, 50);
|
||
}
|
||
|
||
|
||
update() {
|
||
float vx = map(noise(xoff), 0, 1, -maxspeed, maxspeed);
|
||
float vy = map(noise(yoff), 0, 1, -maxspeed, maxspeed);
|
||
//{!1} A little Perlin noise algorithm to calculate a velocity
|
||
PVector velocity = new PVector(vx, vy);
|
||
xoff += 0.01;
|
||
yoff += 0.01;
|
||
|
||
//{!1} The bloop moves.
|
||
position.add(velocity);
|
||
}
|
||
|
||
//{!3} A bloop is a circle.
|
||
display() {
|
||
ellipseMode(CENTER);
|
||
ellipse(this.position.x, this.position.y, this.r, this.r);
|
||
}
|
||
}</pre>
|
||
<p>The above is missing a few details (such as initializing the variables in the constructor), but you get the idea.</p>
|
||
<p>For this example, you'll want to store the population of bloops in an <code>array</code>, rather than an array, as you expect the population to grow and shrink according to how often bloops die or are born. You can store this <code>array</code> in a class called <code>World</code>, which will manage all the elements of the bloops’ world.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class World {
|
||
|
||
//{!1} A list of bloops
|
||
constructor(num) {
|
||
// Start with set of creatures
|
||
this.bloops = []; // An array for all creatures
|
||
for (let i = 0; i < num; i++) {
|
||
let l = createVector(random(width), random(height));
|
||
let dna = new DNA();
|
||
this.bloops.push(new Bloop(l, dna));
|
||
}
|
||
}</pre>
|
||
<p>So far, what I have is just a rehashing of our particle system example from Chapter 5. I have an entity (<code>Bloop</code>) that moves around the window and a class (<code>World</code>) that manages a variable quantity of these entities. To turn this into a system that evolves, I'll need to add two additional features to my world:</p>
|
||
<ul>
|
||
<li><strong><em>Bloops die.</em></strong></li>
|
||
<li><strong><em>Bloops are born.</em></strong></li>
|
||
</ul><a data-type="indexterm" data-primary="fitness functions (natural selection algorithms)" data-secondary="ecosystem simulations and"></a>
|
||
<p>Bloops dying is my replacement for a fitness function, the process of “selection.” If a bloop dies, it cannot be selected to be a parent, because it simply no longer exists! One way I can build a mechanism to ensure bloop deaths in our world is by adding a <code>health</code> variable to the <code>Bloop</code> class.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class Bloop {
|
||
|
||
constructor(l, dna_) {
|
||
this.position = l.copy(); // Location
|
||
//{!1} A bloop is born with 100 health points.
|
||
this.health = 100; // Life timer
|
||
this.xoff = random(1000); // For perlin noise
|
||
this.yoff = random(1000);
|
||
this.dna = dna_; // DNA
|
||
// DNA will determine size and maxspeed
|
||
// The bigger the bloop, the slower it is
|
||
this.maxspeed = map(this.dna.genes[0], 0, 1, 15, 0);
|
||
this.r = map(this.dna.genes[0], 0, 1, 0, 50);
|
||
}</pre>
|
||
<p>In each frame of animation, a bloop loses some health.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> update() {
|
||
// Simple movement based on perlin noise
|
||
let vx = map(noise(this.xoff), 0, 1, -this.maxspeed, this.maxspeed);
|
||
let vy = map(noise(this.yoff), 0, 1, -this.maxspeed, this.maxspeed);
|
||
let velocity = createVector(vx, vy);
|
||
this.xoff += 0.01;
|
||
this.yoff += 0.01;
|
||
|
||
this.position.add(velocity);
|
||
// Death always looming
|
||
this.health -= 0.2;
|
||
}</pre>
|
||
<p>If health drops below 0, the bloop dies.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> // We add a function to the Bloop class
|
||
// to test if the bloop is alive or dead.
|
||
dead() {
|
||
if (this.health < 0.0) {
|
||
return true;
|
||
} else {
|
||
return false;
|
||
}
|
||
}</pre>
|
||
<p>This is a good first step, but I haven’t really achieved anything. After all, if all bloops start with 100 health points and lose 1 point per frame, then all bloops will live for the exact same amount of time and die together. If every single bloop lives the same amount of time, they all have equal chances of reproducing and therefore nothing will evolve.</p><a data-type="indexterm" data-primary="ecosystem simulation genetic algorithms" data-secondary="lifespans" data-tertiary="varying"></a>
|
||
<p>There are many ways I could achieve variable lifespans with a more sophisticated world. For example, I could introduce predators that eat bloops. Perhaps the faster bloops would be able to escape being eaten more easily, and therefore our world would evolve to have faster and faster bloops. Another option would be to introduce food. When a bloop eats food, it increases its health points, and therefore extends its life.</p>
|
||
<p>Let’s assume I have an <code>array</code> of <code>vector</code> positions for food, named “food.” We could test each bloop’s proximity to each food position. If the bloop is close enough, it eats the food (which is then removed from the world) and increases its health.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> eat(f) {
|
||
let food = f.getFood();
|
||
// Are we touching any food objects?
|
||
for (let i = food.length - 1; i >= 0; i--) {
|
||
let foodLocation = food[i];
|
||
let d = p5.Vector.dist(this.position, foodLocation);
|
||
// If we are, juice up our strength!
|
||
if (d < this.r / 2) {
|
||
this.health += 100;
|
||
//{!1} The food is no longer available for other Bloops.
|
||
food.splice(i, 1);
|
||
}
|
||
}
|
||
}</pre>
|
||
<p>Now I have a scenario in which bloops that eat more food live longer and have a greater likelihood of reproducing. Therefore, I expect that our system would evolve bloops with an optimal ability to find and eat food.</p>
|
||
<p>Now that I have built our world, it’s time to add the components required for evolution. First I should establish our genotype and phenotype.</p>
|
||
<h3 id="genotype-and-phenotype">Genotype and Phenotype</h3><a data-type="indexterm" data-primary="ecosystem simulation genetic algorithms" data-secondary="genotype"></a><a data-type="indexterm" data-primary="ecosystem simulation genetic algorithms" data-secondary="phenotype"></a><a data-type="indexterm" data-primary="genotype (natural selection algorithms)" data-secondary="ecosystem simulation"></a><a data-type="indexterm" data-primary="phenotype (natural selection algorithms)" data-secondary="ecosystem simulation"></a>
|
||
<p>The ability for a bloop to find food is tied to two variables—size and speed. Bigger bloops will find food more easily simply because their size will allow them to intersect with food positions more often. And faster bloops will find more food because they can cover more ground in a shorter period of time.</p>
|
||
<figure class="half-width-right">
|
||
<img src="images/09_ga/09_ga_23.png" alt="Figure 9.15">
|
||
<figcaption>Figure 9.15</figcaption>
|
||
</figure>
|
||
<p>Since size and speed are inversely related (large bloops are slow, small bloops are fast), I only need a genotype with a single number.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class DNA {
|
||
|
||
constructor(newgenes) {
|
||
if (newgenes) {
|
||
this.genes = newgenes;
|
||
} else {
|
||
// The genetic sequence
|
||
// DNA is random floating point values between 0 and 1 (!!)
|
||
this.genes = new Array(1);
|
||
for (let i = 0; i < this.genes.length; i++) {
|
||
this.genes[i] = random(0, 1);
|
||
}
|
||
}
|
||
}</pre>
|
||
<p>The phenotype then is the bloop itself, whose size and speed is assigned by adding an instance of a <code>DNA</code> object to the <code>Bloop</code> class.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class Bloop {
|
||
constructor(l, dna) {
|
||
this.position = l.copy(); // Location
|
||
this.health = 200; // Life timer
|
||
this.xoff = random(1000); // For perlin noise
|
||
this.yoff = random(1000);
|
||
this.dna = dna; // DNA
|
||
// DNA will determine size and maxspeed
|
||
// The bigger the bloop, the slower it is
|
||
this.maxspeed = map(this.dna.genes[0], 0, 1, 15, 0);
|
||
this.r = map(this.dna.genes[0], 0, 1, 0, 50);
|
||
}</pre>
|
||
<p>Notice that with <code>maxspeed</code>, the range is mapped to between 15 and 0, meaning a bloop with a gene value of 0 moves at a speed of 15 and a bloop with a gene value of 1 doesn’t move at all (speed of 0).</p>
|
||
<h3 id="selection-and-reproduction">Selection and Reproduction</h3><a data-type="indexterm" data-primary="ecosystem simulation genetic algorithms" data-secondary="reproduction"></a><a data-type="indexterm" data-primary="ecosystem simulation genetic algorithms" data-secondary="selection"></a><a data-type="indexterm" data-primary="reproduction (natural selection algorithms)" data-secondary="ecosystem simulation"></a><a data-type="indexterm" data-primary="selection (natural selection algorithms)" data-secondary="ecosystem simulation"></a>
|
||
<p>Now that I have the genotype and phenotype, I need to move on to devising a means for bloops to be selected as parents. I stated before that the longer a bloop lives, the more chances it has to reproduce. The length of life is the bloop’s fitness.</p>
|
||
<p>One option would be to say that whenever two bloops come into contact with each other, they make a new bloop. The longer a bloop lives, the more likely it is to come into contact with another bloop. (This would also affect the evolutionary outcome given that, in addition to eating food, their ability to find other bloops is a factor in the likelihood of having a baby.)</p>
|
||
<p>A simpler option would be to have “asexual” reproduction, meaning a bloop does not require a partner. It can, at any moment, make a clone of itself, another bloop with the same genetic makeup. If I state this selection algorithm as follows:</p>
|
||
<p><strong><em>At any given moment, a bloop has a 1% chance of reproducing.</em></strong></p>
|
||
<p>…then the longer a bloop lives, the more likely it will make at least one child. This is equivalent to saying the more times you play the lottery, the greater the likelihood you’ll win (though I’m sorry to say your chances of that are still essentially zero).</p>
|
||
<p>To implement this selection algorithm, I can write a function in the <code>Bloop</code> class that picks a random number every frame. If the number is less than 0.01 (1%), a new bloop is born.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> // This function will return a new bloop, the child.
|
||
reproduce() {
|
||
|
||
// A 1% chance of executing the code in
|
||
// this conditional, i.e. a 1% chance of reproducing
|
||
if (random(1) < 0.01) {
|
||
[inline] // Make the Bloop baby
|
||
}
|
||
}</pre>
|
||
<p>How does a bloop reproduce? In our previous examples, the reproduction process involved calling the <code>crossover()</code> function in the <code>DNA</code> class and making a new object from the newly made DNA. Here, since I am making a child from a single parent, I'll call a function called <code>copy()</code> instead.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> reproduce() {
|
||
// asexual reproduction
|
||
if (random(1) < 0.0005) {
|
||
// Child is exact copy of single parent
|
||
let childDNA = this.dna.copy();
|
||
// Child DNA can mutate
|
||
childDNA.mutate(0.01);
|
||
return new Bloop(this.position, childDNA);
|
||
} else {
|
||
return null;
|
||
}
|
||
}</pre>
|
||
<p>Note also that I've reduced the probability of reproducing from 1% to 0.05%. This value makes quite a difference; with a high probability of reproducing, the system will quickly tend towards overpopulation. Too low a probability, and everything will likely quickly die out.</p>
|
||
<p>Writing the <code>copy()</code> function into the <code>DNA</code> class is easy since p5.js includes a function <code>arraycopy()</code> that copies the contents of one array into another.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class DNA {
|
||
|
||
//{!1} This copy() function replaces
|
||
// crossover() in this example.
|
||
copy() {
|
||
// should switch to fancy JS array copy
|
||
let newgenes = [...this.genes];
|
||
return new DNA(newgenes);
|
||
}
|
||
}</pre>
|
||
<p>Now that I have all the pieces in place for selection and reproduction, I can finalize the <code>World</code> class that manages the list of all <code>Bloop</code> objects (as well as a <code>Food</code> object, which itself is a list of <code>vector</code> positions for food).</p>
|
||
<p>Before you run the example, take a moment to guess what size and speed of bloops the system will evolve towards. I'll discuss following the code.</p>
|
||
<div data-type="example">
|
||
<h3 id="example-95-evolution-ecosystem">Example 9.5: Evolution ecosystem</h3>
|
||
<p>https://editor.p5js.org/embed/r1b2jaXOx</p>
|
||
<figure>
|
||
<img src="images/09_ga/09_ga_24.png" alt=" ">
|
||
<figcaption> </figcaption>
|
||
</figure>
|
||
</div>
|
||
<pre class="codesplit" data-code-language="javascript">let world;
|
||
|
||
function setup() {
|
||
createCanvas(640, 360);
|
||
// World starts with 20 creatures
|
||
// and 20 pieces of food
|
||
world = new World(20);
|
||
}
|
||
|
||
function draw() {
|
||
background(175);
|
||
world.run();
|
||
}
|
||
|
||
class World {
|
||
//{!2} The World object keeps track of the
|
||
// population bloops as well as the food.
|
||
constructor(num) {
|
||
// Start with initial food and creatures
|
||
this.food = new Food(num);
|
||
this.bloops = []; // An array for all creatures
|
||
for (let i = 0; i < num; i++) {
|
||
let l = createVector(random(width), random(height));
|
||
let dna = new DNA();
|
||
//{!4 .offset-top} Creating the population
|
||
this.bloops.push(new Bloop(l, dna));
|
||
}
|
||
}
|
||
|
||
// Make a new creature
|
||
born(x, y) {
|
||
let l = createVector(x, y);
|
||
let dna = new DNA();
|
||
this.bloops.push(new Bloop(l, dna));
|
||
}
|
||
|
||
// Run the world
|
||
run() {
|
||
// Deal with food
|
||
this.food.run();
|
||
|
||
// Cycle through the ArrayList backwards b/c we are deleting
|
||
for (let i = this.bloops.length - 1; i >= 0; i--) {
|
||
// All bloops run and eat
|
||
let b = this.bloops[i];
|
||
b.run();
|
||
b.eat(this.food);
|
||
// If it's dead, kill it and make food
|
||
if (b.dead()) {
|
||
this.bloops.splice(i, 1);
|
||
this.food.add(b.position);
|
||
}
|
||
// Perhaps this bloop would like to make a baby?
|
||
//{!2} Here is where each living bloop has
|
||
// a chance to reproduce. As long as a
|
||
// child is made (i.e. not null) it is
|
||
// added to the population.
|
||
let child = b.reproduce();
|
||
if (child != null) this.bloops.push(child);
|
||
}
|
||
}
|
||
}</pre>
|
||
<p>If you guessed medium-sized bloops with medium speed, you were right. With the design of this system, bloops that are large are simply too slow to find food. And bloops that are fast are too small to find food. The ones that are able to live the longest tend to be in the middle, large enough and fast enough to find food (but not too large or too fast). There are also some anomalies. For example, if it so happens that a bunch of large bloops end up in the same position (and barely move because they are so large), they may all die out suddenly, leaving a lot of food for one large bloop who happens to be there to eat and allowing a mini-population of large bloops to sustain themselves for a period of time in one position.</p>
|
||
<p>This example is rather simplistic given its single gene and asexual reproduction. Here are some suggestions for how you might apply the bloop example in a more elaborate ecosystem simulation.</p>
|
||
<div data-type="project">
|
||
<h3 id="the-ecosystem-project-8">The Ecosystem Project</h3>
|
||
<p>Step 9 Exercise:</p>
|
||
<p>Add evolution to your ecosystem, building from the examples in this chapter.</p>
|
||
<ul>
|
||
<li>Add a population of predators to your ecosystem. Biological evolution between predators and prey (or parasites and hosts) is often referred to as an “arms race,” in which the creatures continuously adapt and counter-adapt to each other. Can you achieve this behavior in a system of multiple creatures?</li>
|
||
<li>How would you implement crossover and mutation between two parents in an ecosystem modeled after the bloops? Try implementing an algorithm so that two creatures meet and mate when within a certain proximity. Can you make creatures with gender?</li>
|
||
<li>Try using the weights of multiple steering forces as a creature’s DNA. Can you create a scenario in which creatures evolve to cooperate with each other?</li>
|
||
<li>One of the greatest challenges in ecosystem simulations is achieving a nice balance. You will likely find that most of your attempts result in either mass overpopulation (followed by mass extinction) or simply mass extinction straight away. What techniques can you employ to achieve balance? Consider using the genetic algorithm itself to evolve optimal parameters for an ecosystem.</li>
|
||
</ul>
|
||
</div>
|
||
</section> |