Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Metrics and visualizations

notebooks/metrics.livemd

Metrics and visualizations

Mix.install([
  {:meow, "~> 0.1.0-dev", github: "jonatanklosko/meow"},
  {:nx, "~> 0.3.0"},
  {:exla, "~> 0.3.0"},
  {:vega_lite, "~> 0.1.6"},
  {:kino_vega_lite, "~> 0.1.4"}
])

Nx.Defn.global_default_options(compiler: EXLA)

Setup

As before, we first install the necessary dependencies. This time we also add VegaLite for plotting our data and the corresponding Livebook integration - KinoVegaLite. In fact, Meow provides a number of predefined plots that we will explore!

Recording metrics

In the introductory notebook we worked with the Rastrigin function. We will continue that story, this time having a closer look on how the algorithms behave over time. Again, we will work in 100 dimensions.

defmodule Problem do
  import Nx.Defn

  def size, do: 100

  @two_pi 2 * :math.pi()

  defn evaluate_rastrigin(genomes) do
    sums =
      (10 + Nx.power(genomes, 2) - 10 * Nx.cos(genomes * @two_pi))
      |> Nx.sum(axes: [1])

    -sums
  end
end

Let’s start with a simple algorithm with four populations evolving independently.

Note that we use the log_metrics operation to calculate all of the listed metrics as the population evolves. The MeowNx.Metric module already comes with a number of metrics, but those are just simple numerical definitions, so you can easily plug in your own!

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_replace_uniform(0.001, -5.12, 5.12),
      MeowNx.Ops.log_best_individual(),
      MeowNx.Ops.log_metrics(
        %{
          fitness_max: &MeowNx.Metric.fitness_max/2,
          fitness_min: &MeowNx.Metric.fitness_min/2,
          fitness_sd: &MeowNx.Metric.fitness_sd/2,
          genomes_mean_distance: &MeowNx.Metric.genomes_mean_euclidean_distance/2
        },
        interval: 10
      ),
      Meow.Ops.max_generations(500)
    ]),
    duplicate: 4
  )

# And run the algorithm
report = Meow.run(algorithm)

:ok

After running the algorithm we get a bunch of information that we store in the report variable. We already know how to extract a brief summary out of it:

report |> Meow.Report.format_summary() |> IO.puts()

So this tells us what the best individual is, but we collected a bunch of metrics along the way, let’s see them!

Meow.Report.plot_metrics(report)

There we go, all metrics ready to analyze. The plots are interactive so we can zoom in and out freely!

This plots all the metrics in one shot, but we can also explicitly pick a metric to visualize.

Meow.Report.plot_metric(report, :fitness_max)

Or alternatively

Meow.Report.plot_metric(report, :fitness_max, arrange: :grid)

Finally, we can see how long each population took, both time-wise and generation-wise.

Meow.Report.plot_times(report)
Meow.Report.plot_generations(report)

This is not particularly useful for our simple, homogenous algorithm, but can provide some insights in more complex cases or when running in distributed setup!

Heterogenous algorithms

With these new tools under the belt, let’s try out a more interesting algorithm. To take it one step at a time, we will keep the populations independent, but we will use two different pipelines, two populations per each.

The pipelines itself should look familiar, since we used these in the introductory notebook. Also note that we keep the limit of generations at 1000, so we can iterate on the algorithm more quickly.

# The pipelines are composable, so we can share the common pieces

initializer_op = MeowNx.Ops.init_real_random_uniform(100, Problem.size(), -5.12, 5.12)

metrics_op =
  MeowNx.Ops.log_metrics(
    %{
      fitness_max: &MeowNx.Metric.fitness_max/2,
      fitness_sd: &MeowNx.Metric.fitness_sd/2
    },
    interval: 10
  )

algorithm =
  Meow.objective(&Problem.evaluate_rastrigin/1)
  |> Meow.add_pipeline(
    initializer_op,
    Meow.pipeline([
      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.emigrate(MeowNx.Ops.selection_natural(5), &Meow.Topology.ring/2, interval: 10),
      # Meow.Ops.immigrate(&MeowNx.Ops.selection_natural(&1), interval: 10),
      metrics_op,
      Meow.Ops.max_generations(1000)
    ]),
    duplicate: 2
  )
  |> Meow.add_pipeline(
    initializer_op,
    Meow.pipeline([
      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)
        ])
      ]),
      # 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(),
      metrics_op,
      Meow.Ops.max_generations(1000)
    ]),
    duplicate: 2
  )

# Execute the above algorithm

report = Meow.run(algorithm)
report |> Meow.Report.format_summary() |> IO.puts()

Note: populations are ordered in the same manner as pipelines, so in this case populations 0 and 1 evolve according to the first one, while populations 2 and 3 according to the second one.

Meow.Report.plot_metrics(report)

The populations evolve independently, so we can see that both pipelines are similarly effective and gradually improve over time. With two solid sub-algorithms at hand we can try introducing a migration step, so that populations can benefit from each other!

Take a note of what the best fitness is, then uncomment the migration steps in both pipelines and see how the final algorithm performs!