Skip to content

Blog

The Importance of Parameter Tuning In Evolutionary Algorithms

This is an extension of a blog post I wrote a couple months ago. You can find it here.

One of the big takeaways in my introduction to Evolutionary Algorithms was the sheer number of numerical parameters.

  • \(\mu\) And \(\lambda\)
  • Mutation Rate
  • \(k\) in k-Tournament Selection

Not only this, but the sheer number of parameters:

  • The genotype
  • The mutator operator
  • The survivor selection algorithm

And one might be wondering, what is the best operator for \(x\) or \(y\)? Let’s look at an example.

Recall the problem from the previous discussion: We are going to consider a sample problem, a deciphering program. The premise of the problem is as follows:

There is a string of characters (without spaces) hidden away that, after set, is inaccessible.
There are two ways to retrieve data about the hidden message:

1: Get the length of the string.

2: Given a string, the problem will output how many characters match within the two strings.

Disregarding the other technical details, let us focus on the survivor selection. We used \(k\) -tournament selection (with \(k = 50\)). But, let’s run a little experiment:

Run the Evolutionary Algorithm, with \(k\) ranging from \(5\) (basically the bare minimum) to \(100\) (\(\lambda\), the population size), and see how fast the algorithm terminates. Do this 1,000 times to get accurate results.

The result?

This makes sense. Our problem has one local optimum: the actual solution. So we do not need a lot of genetic diversity, we need aggressive selective pressure[1] to reach the top quickly.

As \(k\) gets closer to \(\mu\), the average termination time decreases. What does this tell us? We picked the wrong survivor selection algorithm.

With \(k = \mu\), we no longer have \(k\) -tournament selection; we have truncation selection (where only the most fit individuals survive). And that's the interesting part about Evolutionary Algorithms: there are no objective, best parameters.

How do we alleviate this? Trial and error. There is no telling when one parameter is going to perform better than another.

After a couple of trial runs, and objectives in mind (average terminating fitness, best terminating fitness, time to termination), the answer might surprise (and delight) you.


  1. How elitist the survivor selection algorithm is, picking the strongest individuals more often. ↩︎

The Reusability Of Evolutionary Algorithms

3-SAT Solving With EAs

Let's propose an Evolutionary Algorithm experiment; say we already have a framework in place (like the Secret Message framework we previously implemented). How difficult would it be to completely switch problem instances?

First, we need another problem instance. Our previous problem instance was pretty straightforward: it had one local optimum. Let's take on a problem with many local optima, such as the 3-SAT problem.

The premise of 3-SAT is simple. From a global pool of variables (\(x_1\), \(x_2\), \(\ldots\), \(x_n\)), we have a basic clause of three variables or-ed together (signified by \(\vee\)):

$$x_p \vee x_q \vee x_r$$

Then, and (signified by a \(\wedge\)) several clauses together:

$$\left(x_p \vee x_q \vee x_r\right) \wedge \left(x_s \vee x_t \vee x_u\right) \wedge \ldots \wedge \left(x_v \vee x_w \vee x_y\right)$$

The only stipulation is that any variable can be negated (signified by a \(\neg\)). So, supposing we want to negate \(x_p\); \(x_s\) and \(x_u\); and \(x_v\), \(x_w\), and \(x_y\); we can do the following:

$$\left(\neg x_p \vee x_q \vee x_r\right) \wedge \left(\neg x_s \vee x_t \vee \neg x_u\right) \wedge \ldots \wedge \left(\neg x_v \vee \neg x_w \vee \neg x_y\right)$$

Now, we simply have to assign all the variables such that all the clauses will evaluate to true. It may sound simple, but it belongs to the hardest classes of problems in computer science. There is no guaranteed algorithm to produce the right answer at this time.

For a more visual approach, please reference the figure below. The goal is to make every inner node green, by having at least one connected outer node be green. Note the green nodes have to account for negation as well.

This sounds like a good problem candidate for an Evolutionary Algorithms.

The SAT Problem

We can skip over the problem specific parts to worry more about the Evolutionary Algorithm aspect. Suppose we already have a well-defined SAT class that takes care of SAT-specific properties and methods, like so:

class SAT:
    def __init__(self, filename):
        """Create a SAT object that is read in from a CNF file."""
        ...

    @property
    def variables(self):
        """Get *all* the variables."""
        ...

    @property
    def total_clauses(self):
        """Get the total number of clauses."""
        ...

    @property
    def clauses_satisfied(self):
        """Get the number of satisfied clauses."""
        ...

    def __getitem__(self, key):
        """Get a particular variable (key)"""
        ...

    def __setitem__(self, key, value):
        """Set a variable (key) to value (True/False)"""
        ...

From this, we can create a new genotype for our Individual.

The New Genotype

The genotype structure was very similar to what we had before:

  • The genotype is the SAT problem we defined above.
  • Fitness is defined by a percentage of the total satisfied clauses.
  • Mutation is uniform, choose a percentage \(p\) of alleles and flip their value.
  • Recombination is uniform, randomly assemble values from both parents.

Looking at the refactoring, not much has changed.

The New EA Framework

Now that we have updated our Individual, the next thing to update would be the Evolutionary Algorithm framework, including:

  • The Population
  • The EA Itself

Except, we don't have to.

That's the beauty of Evolutionary Algorithms, they are incredibly adaptable. By swapping out the Individual, the rest of the evolutionary algorithm should still work.

For our SAT problem, there were some parameters updated, to make the algorithm more efficient:

  • The mutation rate has been reduced to 5%
  • The tournament size has been reduced to 15 individuals (out of \(\lambda = 100\)).

The Result

So, let's try our Evolutionary Algorithm. Taking a SAT instance with 75 variables and 150 clauses, this makes the search space

$$2^{75} \approx 3.77 \times 10^{22}$$

Great, so roughly 1,000 times the grains of sand on Earth, easy. So, can our EA do it?

After roughly 100 iterations, yes. See the visualization below.

Marvelous, our EA managed to find a solution after only 100 iterations in a giant search space. And all we had to do was swap out one class.

On Being Passionate About Tools

Get to work. Grab a cup of coffee, some water. Possibly breakfast.

Sit down at my desk. Log into my computer, look at i3wm.

Launch a new terminal session. ranger. Enter. Navigate to project. nvim. Enter.

space f. Use fzf to open necessary file.

This is a daily workflow for me. Before I start typing code, I use about four tools to get to it. Notice, I haven’t mentioned touching the mouse once yet; Because I haven't touched the mouse yet.

Throughout my academic and professional career as a software engineer, I spent a lot of time using tools. Trying new tools. Searching for tools. Customizing tools[1].

Although this might be a commendable feat, it is often met with skepticism. A lot of skepticism.

I've had at least a hundred conversations with people about tools, about why they advocate against using such tools. These conversations were with other students, senior software engineers, and tenured professors—and generally, almost all of them fell into three distinct buckets of people.

It’s a waste of time.

This is by far the worst yet best counterargument against investing so heavily into tools—and the reasoning is in the trailing three words.

waste. Better tools will objectively help one write more code. If there is a modification that needs to be made at the end of the file, it takes one keystroke to jump to that position in Vim. Picture doing this, but 100s of times a day.

of time. There is a time commitment, a big one. And there is a learning curve, and it is steep. So, one would have to justify the reasons for learning it. And I've done so below.

What I have works just fine.

I’m deeply skeptical of the “If it’s not broken, don’t fix it” mentality. There’s always room for improvement, and there’s nothing more I love to see than improvement.

It’s why we buy new phones. It’s why we like nice, new cars. It’s why we, as civilization, love progress.

Even if it’s a marginal increase, say 5% more code a week. With a 100 lines of code written a week, that can be a about 250 additional lines of code written a year. A marginal increase, sure; but a welcomed one.

It doesn’t really make you a better programmer.

I generally have three core reasons against this, and they are as follows.

It'll help write more code, faster. It is objectively faster to type more code if your hands never leave the keyboard. It's faster to navigate a filesystem without having to focus on where a cursor is. It's ridiculously faster to open a file by just typing out parts of the filename as opposed to navigating to it.

It's healthier. Legitimately. Minimizing hand travel will significantly put less strain on your hands, wrists, and arms.

It'll make you a better programmer. As programmers, we have to learn things all the time. New programming languages, new paradigms, new company software. Learning a new tool keeps a mind sharp by reinforcing how to learn something. Plus, the skills are fluid. Learning to navigate in Vim will help navigate in ranger. Learning regular expressions can be used in fuzzy searches. The skills are transferable.

So, where might one start? Again, there are three areas I see that can have the most benefit from improving your workflow.

Your operating system. Whether it’s Windows, MacOS, or Linux, you have to use an operating system. Get good at it. Learn the paradigms of the system. Learn how the file explorer works in Windows. Learn advanced features in MacOS like a spotlight alternative or simple hot corners. Or learn how to set up your WiFi drivers in Linux.

Your text editor. Choose ones that are portable. I use NeoVim. Many people have success with Emacs. Some people prefer a visual editor like Sublime Text or Atom. Learn their workflows. Learn the shortcuts of Emacs. Learn how to edit text in Normal Mode using Vim. Install packages in Sublime or Atom. You spend most of your time here anyway, might as well get familiar with it.

The intermediary layer between your operating system and your text editor. By this, I usually mean the shell, like Bash or ZSH. Learn how to grep or navigate around. Install packages to make tasks easier. It’s well worth it.

So, yes, I am a strong advocate of using advanced tools. Established tools. Great tools. And I hope I’ve convinced you to do the same.


  1. 145 commits worth. ↩︎

Calculating Euler's Number

We are well aware of Euler’s famous number,

$$
e = 2.71828182845
$$

There are also quite a few ways of deriving Euler’s Number. There’s the Taylor expansion method:

$$
e = \sum _{k=0} ^{\infty} \frac{1}{k!}
$$

There is also the classical limit form:

$$
e = \lim_{n \rightarrow \infty} \left( 1 + \frac{1}{n} \right)^n
$$

Then there is another. Let \(R\) be a random number generated between \([0, 1]\), inclusive. Then \(e\) is the average of the number of \(R\)s it takes to sum greater than \(1\).

With more rigor, for uniform \((0,\, 1)\) random independent variables \(R_1\), \(R_2\), \(\ldots\), \(R_n\),

$$
N = \min \left\{n: \sum_{k=1}^n R_k > 1 \right\}
$$

where

$$e = \text{Average}(n)$$

The proof can be found here, but it’s pretty math-heavy. Instead, an easier method is to write a program to verify for large enough \(n\).

n Sum Solution Limit Solution Random Uniform Variable
1 1 2 2
10 1.7182818011 2.5937424601 2.5
100 1.7182818284 2.7048138294 2.69
1000 1.7182818284 2.7169239322 2.717
10000 2.7181459268 2.7242
100000 2.7182682371 2.71643
1000000 2.7182804690 2.71961
10000000 2.7182816941 2.7182017
100000000 2.7182817983 2.71818689
1000000000 2.7182820520 2.718250315

Source Code

def e_sum(upper_bound):
	if upper_bound < 1:
		return 0

	return Decimal(1.0) / Decimal(math.factorial(upper_bound)) + \
	       Decimal(e_sum(upper_bound - 1))

def e_limit(n):
	return Decimal((1 + 1.0 / float(n))**n)


def find_greater_than_one(value=0, attempts=0):
	if value <= 1:
		return find_greater_than_one(value + random.uniform(0, 1), attempts + 1)

	return attempts