Run this notebook

Use Livebook to open this notebook and explore new ideas.

It is easy to get started, on your machine or the cloud.

Click below to open and run it in your Livebook at .

(or change your Livebook location)

# Rastrigin ```elixir Mix.install([ {:meow, "~> 0.1.0-dev", github: "jonatanklosko/meow"}, {:nx, "~> 0.7.0"}, {:exla, "~> 0.7.0"} ]) Nx.Defn.global_default_options(compiler: EXLA) ``` ## Setup In the above cell we bring in the necessary dependencies. Until the first release Meow needs to be installed directly from GitHub. We also need [Nx](https://github.com/elixir-nx/nx/tree/main/nx) for numerical definitions and [EXLA](https://github.com/elixir-nx/nx/tree/main/exla) to compile them effectively. ## Objective The first step is defining the optimisation objective function. In this example we will be working with the [Rastrigin function](https://en.wikipedia.org/wiki/Rastrigin_function), which looks like so: ![](https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Rastrigin_function.png/800px-Rastrigin_function.png) The function has its minimum in **0** and it actually has the value of 0. However, as you can see the function has numerous local optima, which could pose problems to some optimisation methods. We will see how evolutionary algorithms can be of help here. Rastrigin function is easily generalized to any number of dimensions, allowing to adjust the difficulty of the problem. ### Fitness evaluation In evolutionary terms, the objective function is referred to as fitness, with every solution being a single individual. An evolutionary algorithm tries to find individuals with the highest fitness (thus the best solutions). Translating this into Meow, we need to define the fitness function, but instead of assessing a single solution, the function should assess a whole group of them. This approach gives you more freedom in terms of implementation. In the example we will represent the population as a 2-dimensional Nx tensor, and consequently we can calculate fitness for all individuals at once! ```elixir defmodule Problem do import Nx.Defn def size, do: 100 @two_pi 2 * :math.pi() defn evaluate_rastrigin(genomes) do sums = (10 + Nx.pow(genomes, 2) - 10 * Nx.cos(genomes * @two_pi)) |> Nx.sum(axes: [1]) -sums end end ``` The above numerical definition receives a 2-dimensional tensor, where every row is a single solution. It then calculates the value of Rastrigin function for each of them and returns that as a 1-dimensional tensor. Note that we actually flip the function (multiply the values by -1), so that we deal with maximisation problem. ## The first algorithm In Meow an evolutionary algorithm is defined as a pipeline of operations that the population goes through until the termination criteria is met. This approach is heavily declarative and gives us much flexibility. You can think of it as composing the algorithm out of basic building blocks. Let's define our first algorithm. We will initialize the population with 100 random individuals, each with 100 floating-point genomes (meaning we optimise 100-dimensional Rastrigin function). ```elixir algorithm = Meow.objective( # Specify the evaluation function that we are trying to maximise &Problem.evaluate_rastrigin/1 ) |> Meow.add_pipeline( # Initialize the population with 100 random individuals MeowNx.Ops.init_real_random_uniform(100, Problem.size(), -5.12, 5.12), # A single pipeline corresponds to a single population Meow.pipeline([ # Define a number of evolutionary steps that the population goes through MeowNx.Ops.selection_tournament(1.0), MeowNx.Ops.crossover_uniform(0.5), MeowNx.Ops.mutation_replace_uniform(0.001, -5.12, 5.12), MeowNx.Ops.log_best_individual(), Meow.Ops.max_generations(5_000) ]) ) # Execute the above algorithm report = Meow.run(algorithm) report |> Meow.Report.format_summary() |> IO.puts() ``` By looking at the best genome we can infer that the solution we are looking for is close **0**. But let's explore other algorithms! ## Non-linear pipeline In the above algorithm, we replace the whole population with new 100 individuals every generation. This means that promising solutions may be lost by unfortunate crossover. To overcome this, we could keep the best 20% individuals in the population and replace the remaining 80% with new ones. This can be achieved by splitting the pipeline into two branches: * one that selects the best 20% of individuals * one that generates 80% new individuals using the evolutionary operations Then we can just combine the results into a single population. Let's see this in practice, together with other crossover and muation types. ```elixir algorithm = Meow.objective(&Problem.evaluate_rastrigin/1) |> Meow.add_pipeline( MeowNx.Ops.init_real_random_uniform(100, Problem.size(), -5.12, 5.12), Meow.pipeline([ # Here the pipeline branches out into two sub-pipelines, # which results are then joined into a single population. Meow.Ops.split_join([ Meow.pipeline([ MeowNx.Ops.selection_natural(0.2) ]), Meow.pipeline([ MeowNx.Ops.selection_tournament(0.8), MeowNx.Ops.crossover_blend_alpha(0.5), MeowNx.Ops.mutation_shift_gaussian(0.001) ]) ]), MeowNx.Ops.log_best_individual(), Meow.Ops.max_generations(5_000) ]) ) report = Meow.run(algorithm) report |> Meow.Report.format_summary() |> IO.puts() ``` This gives us a much more precise solution! The fitness is closer to 0 by several orders of magnitude, and it's pretty clear that the solution is the **0** vector. ## Multi-population algorithm So far we saw two different algorithms, both involving just a single population. Meow does not stop there and allows us to define multi-population algorithms! Introducing multiple populations is as easy as adding multiple pipelines to the algorithm - each for a single population. However, this alone isn't much different from running the algorithm several times in a row. We need to introduce some communication between the populations! One technique is called the Island Model, where each population evolves on its own island and once in a while some individuals migrate from one island to another. Let's modify the first algorithm to include migration steps. We will emigrate 5 best individuals every 10 generations and use the ring topology. ```elixir algorithm = Meow.objective(&Problem.evaluate_rastrigin/1) |> Meow.add_pipeline( MeowNx.Ops.init_real_random_uniform(100, Problem.size(), -5.12, 5.12), Meow.pipeline([ MeowNx.Ops.selection_tournament(1.0), MeowNx.Ops.crossover_uniform(0.5), MeowNx.Ops.mutation_shift_gaussian(0.001), Meow.Ops.emigrate(MeowNx.Ops.selection_natural(5), &Meow.Topology.ring/2, interval: 10), Meow.Ops.immigrate(&MeowNx.Ops.selection_natural(&1), interval: 10), MeowNx.Ops.log_best_individual(), Meow.Ops.max_generations(5_000) ]), # Instead of calling Meow.add_pipeline 3 times, # we can just tell it to replicate the pipeline # that number of times duplicate: 3 ) report = Meow.run(algorithm) report |> Meow.Report.format_summary() |> IO.puts() ```
See source

Have you already installed Livebook?

If you already installed Livebook, you can configure the default Livebook location where you want to open notebooks.
Livebook up Checking status We can't reach this Livebook (but we saved your preference anyway)
Run notebook

Not yet? Install Livebook in just a minute

Livebook is open source, free, and ready to run anywhere.

Run on your machine

with Livebook Desktop

Run in the cloud

on select platforms

To run on Linux, Docker, embedded devices, or Elixir’s Mix, check our README.

PLATINUM SPONSORS
SPONSORS
Code navigation with go to definition of modules and functions Read More ×