symbolic_regression_part3 - morinim/ultra GitHub Wiki

Symbolic Regression - custom Evaluator and teams

A custom evaluator is often not enough. In some problems, we need to evolve multiple programs simultaneously and evaluate them as a whole.

Can this be done?

Yes, it's possible using a team.

(Not so) toy problem

$$ \begin{pmatrix} a_1 \\ a_2 \\ \vdots \\ a_N \end{pmatrix} = \begin{pmatrix} b_{11} & b_{12} & \ldots & b_{1N} \\ b_{21} & b_{22} & \ldots & b_{2N} \\ \vdots & \vdots & \vdots & \vdots \\ b_{N1} & b_{N2} & \ldots & b_{NN} \end{pmatrix} \cdot \begin{pmatrix} \boldsymbol{f_1}(c) \\ \boldsymbol{f_2}(c) \\ \vdots \\ \boldsymbol{f_N}(c) \end{pmatrix} $$

This extends the previous problem: instead of evolving a single function, we now evolve a set of functions that are evaluated jointly.

Setting up code

const auto a = get_vector();  // N-dimensional vector
const auto b = get_matrix();  // NxN matrix

This time, a and b are no longer scalars. We use helper functions to generate a random vector and a matrix of compatible dimensions.

using candidate_solution = ultra::gp::team<ultra::gp::individual>;

The candidate solution is now a team of individuals (i.e. multiple programs evolved together). Conceptually, each individual in the team learns a component of the solution, and the final output emerges from their combination.


Once again, the evaluator needs various changes:

// Given a team (i.e. a candidate solution of the problem), returns a score
// measuring how good it is.
[[nodiscard]] double my_evaluator(const candidate_solution &x)
{
  using namespace ultra;

  std::vector<double> f(N);
  std::ranges::transform(
    x, f.begin(),
    [](const auto &prg)
    {
      const auto ret(run(prg));

      return has_value(ret) ? std::get<D_DOUBLE>(ret) : 0.0;
    });

  std::vector<double> model(N, 0.0);
  for (unsigned i(0); i < N; ++i)
    for (unsigned j(0); j < N; ++j)
      model[i] += b(i, j) * f[j];

  double delta(std::inner_product(a.begin(), a.end(), model.begin(), 0.0,
                                  std::plus{},
                                  [](auto v1, auto v2)
                                  {
                                    return std::fabs(v1 - v2);
                                  }));

  return -delta;
}

The members of a team are always selected, evaluated, and varied together, effectively undergoing a co-evolutionary process.

A line-by-line description of the evaluation process follows:

std::vector<double> f(N);
std::ranges::transform(
  x, f.begin(),
  [](const auto &prg)
  {
    const auto ret(run(prg));

    return has_value(ret) ? std::get<D_DOUBLE>(ret) : 0.0;
  });

Here, f is a vector storing the outputs of each program in the team.

std::vector<double> model(N, 0.0);
for (unsigned i(0); i < N; ++i)
  for (unsigned j(0); j < N; ++j)
    model[i] += b(i, j) * f[j];

Mathematically the code is equivalent to:

$$ model[i] = \sum_{j=0}^{N-1} b_{ij} \cdot f_j(c) $$

This corresponds to a standard matrix–vector multiplication.

As before, delta measures the error using the absolute difference:

double delta(std::inner_product(a.begin(), a.end(), model.begin(), 0.0,
                                std::plus{},
                                [](auto v1, auto v2)
                                {
                                  return std::fabs(v1 - v2);
                                }));

std::inner_product performs an ordered map/reduce operation on a and model. Mathematically:

$$ delta = \sum_{i=0}^{N-1} \left\lvert a_i - model[i] \right\rvert $$


Only one line of the main() function varies:

prob.params.team.individuals = N;

to inform the search engine of the team size.

(for your ease, all the code is in the examples/symbolic_regression04.cc file)


PROCEED TO PART 4→

⚠️ **GitHub.com Fallback** ⚠️