Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

symbolic_regression_part3

Manlio Morini edited this page Mar 25, 2026 · 36 revisions

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→

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /