Rotated and shifted problems

In this tutorial we will learn how to use meta-problems. These are optimization problems that transform somehow another optimization problem. In particular we will have a look to the rotated and shifted meta-problems. Let us start creating a shifted problem.


In [ ]:
from PyGMO import problem
prob = problem.ackley(5)
shifted_prob1 = problem.shifted(problem=prob)
shifted_prob2 = problem.shifted(problem=prob, shift=15)
shifted_prob3 = problem.shifted(problem=prob, shift=[23, -12.2, 22, 33, 5.3])

We have used three different constructors to instantiate the new problem with a random shift vector (shifted_prob1), with a uniform shift vector (shifted_prob2) and with a fully defined shift vector (shifted_prob3). In all cases we may extract the shift vector using the corresponding attribute


In [ ]:
print(shifted_prob1.shift_vector)

We may now check that such a shift does not change the performance of a given algorithm. We choose, for this tutorial Improved Harmony Search, but you can try changing it to test others.


In [ ]:
from PyGMO import algorithm, population
import matplotlib.pyplot as plt

def compare_problems(prob, prob_meta):
    # Evolve 100 populations for each problem and compare
    # the champions' objective function distribution
    l = []
    algo = algorithm.ihs(1000);
    for _ in range(100):
        pop = population(prob, 20)
        for i in range(15):
            pop = algo.evolve(pop)
        l.append(pop.champion.f[0])
    l_meta = []
    for _ in range(100):
        pop = population(prob_meta, 20)
        for _ in range(15):
            pop = algo.evolve(pop)
        l_meta.append(pop.champion.f[0])
        
    plt.ylabel("Objective function")
    plt.yscale('log')
    plt.xticks([0, 1], ['Original', 'Meta problem'])
    plt.boxplot([l, l_meta])
    plt.show()

In [ ]:
# Call the function for shifted problem
compare_problems(prob, shifted_prob1)

We can clearly see how the algorithm ihs does not change its performances when the search space is shifted.

We now repeat the same procedure for a rotated problem. Also in the case of the rotated problem the kwarg rotation allows to pass a rotation matrix directly, otherwise a random orthonormal matrix will be generated and can be extracted by the problem.rotation attribute.


In [ ]:
prob = problem.ackley(5)
rotated_prob = problem.rotated(problem=prob)
compare_problems(prob, rotated_prob)

Which clearly indicates how the rotation affects negatively the algorithm performance.

Note that meta-problems can be nested together, so it is perfectly valid to have


In [ ]:
prob = problem.ackley(5)
new_prob = problem.rotated(problem.shifted(problem.rotated(prob)))

To make sure one can reconstruct the original problem, the transformations applied are logged in the problem __repr__ method

The noisy meta-problems

The noisy meta-problem introduces noise into regular PyGMO problem, rendering them stochastic. More specifically, the observed fitness and constraint vectors from such problems have been corrupted by noises. Currently, two types of noise distributions are supported, namely the normal distribution and uniform distribution.


In [ ]:
from PyGMO import *
prob = problem.ackley(1)
prob_noisy_normal = problem.noisy(prob, trials=1, param_first=0.0, param_second=3.0, noise_type=problem.noisy.noise_distribution.NORMAL)
prob_noisy_uniform = problem.noisy(prob, trials=1, param_first=-3.0, param_second=3.0, noise_type=problem.noisy.noise_distribution.UNIFORM)

The construction parameters control the following aspects of the noisy transform:

  • Noise distribution: The Gaussian noise is characterized by a mean of param_first and a standard deviation of param_second, while the uniformly noise is uniformly distributed between param_first and param_second.
  • Internal averaging: The noisy meta-problem provides an internal mechanism to perform averaging via repeated evaluation before reporting the fitness / constraint vectors, at the expense of increase computational budget. The number of samples to average over is controlled by the parameter trials.

The fitness landscape (or more precisely the distribution of fitness values at each decision value) of a noisy one-dimensional Ackley problem is visualized in the following figures. The solid line is the fitness in case of a noise-less problem.


In [ ]:
import numpy as np
import matplotlib.pyplot as plt

# Set up plotting canvas
fig = plt.figure(figsize=(12, 4))
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122)
for ax in [ax1, ax2]:
    ax.set_ylim(-10, 30)
    ax.set_xlim(-15, 30)
ax1.set_title("1D Ackley [Noisy NORMAL(0.0, 3.0)]")
ax2.set_title("1D Ackley [Noisy UNIFORM(-3.0, 3.0)]")

# Plot the non-noised reference
xs = np.linspace(-15, 30, 200)
ys = [prob.objfun([xsi, ]) for xsi in xs]
ax1.plot(xs, ys, c='k')
ax2.plot(xs, ys, c='k')

n_trials = 200
# Plot the normal-noised function
import random
for i in range(n_trials):
    seed = random.randint(1, 100000)
    prob_noisy_normal.seed = seed
    ys_noised = [prob_noisy_normal.objfun([xsi, ]) for xsi in xs]
    ax1.scatter(xs, ys_noised, s=1, c='k', alpha=0.02)

# Plot the uniform-noised function
for i in range(n_trials):
    seed = random.randint(1, 100000)
    prob_noisy_uniform.seed = seed
    ys_noised = [prob_noisy_uniform.objfun([xsi, ]) for xsi in xs]
    ax2.scatter(xs, ys_noised, s=1, c='k', alpha=0.02)

The transformed problem becomes stochastic. It can be solved by optimizers capable of handling stochastic problems, for example pso_gen. The quality of a solution can be assessed by the noise-less version of the problem, serving as the ground truth information.

As some examples, let’s apply pso_gen to different version of noisy problems. Generally, the larger the magnitude of noise, the harder the problem becomes. Increasing the trials parameter reduces the adversarial effect of noise.


In [ ]:
# NOTE: This script can take up to 1 minute to finish

# Set up plotting canvas
fig = plt.figure(figsize=(12, 4))
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122)
ax1.set_title("Noised (NORMAL(0, x), trials=1) Ackley(D=10)")
ax1.set_xlabel("Standard deviation of noise")
ax1.set_ylabel("Objective function value")
ax2.set_xlabel("Number of trials")
ax2.set_ylabel("Objective function value")
ax2.set_title("Noised (NORMAL(0, 0.3), trials=x) Ackley(D=10)")

# Set up the problem for varying STD test
prob = problem.ackley(10)
l = []
for i in range(11):
    prob_noisy_normal = problem.noisy(prob, trials=1, param_first=0.0,
                                      param_second=float(i) / 10, 
                                      noise_type=problem.noisy.noise_distribution.NORMAL)
    d = []
    for j in range(30):
        seed = random.randint(1, 100000)
        prob_noisy_normal.seed = seed
        alg = algorithm.pso_gen(gen=100)
        pop = population(prob_noisy_normal, 40)
        pop = alg.evolve(pop)
        d.append(pop.champion.f[0])
    l.append(d)
ax1.boxplot(l)
ax1.set_xticklabels(["{:.1f}".format(float(i)/10) for i in range(11)])

# Set up the varying trials parameter test
l = []
for tr in [1, 5, 10, 20, 40]:
    prob_noisy_normal = problem.noisy(prob, trials=tr, param_first=0.0,
                                      param_second=0.3, 
                                      noise_type=problem.noisy.noise_distribution.NORMAL)
    d = []
    for j in range(30):
        seed = random.randint(1, 100000)
        prob_noisy_normal.seed = seed
        alg = algorithm.pso_gen(gen=100)
        pop = population(prob_noisy_normal, 40)
        pop = alg.evolve(pop)
        d.append(pop.champion.f[0])
    l.append(d)
ax2.boxplot(l)
ax2.set_xticklabels(["{:d}".format(i) for i in [1, 5, 10, 20, 40]])

plt.show()

Decomposition

In this tutorial we will learn how to use the decompose meta-problems to solve a multi-objective problem. The decompose meta-problem transforms a multi-objective problem into a single-objective one having as fitness function a convex combination (defined by a weight vector) of the original objectives. Let us start creating a decomposed problem from a multi-objective one.


In [ ]:
from PyGMO import problem
orig_prob = problem.zdt(1, 10)
prob = problem.decompose(problem=orig_prob, weights=[0.5, 0.5])

In this way the 2 objectives of the original problem are equally weighted. If we don’t define the weight vector, then it is randomly generated.

We then proceed by solving the new decomposed problem using a single-objective optimization algorithm, and see how the fitness of the new problem is equal to the average of the original 2 objectives


In [ ]:
alg = algorithm.jde(50)
pop = population(prob, 200)
for i in xrange(5):
    pop = alg.evolve(pop)
    print("Generation {}".format(i))
    print("Distance from Pareto Front (p-distance): {}".format(orig_prob.p_distance(pop)))
    print("Original fitness: {}".format(orig_prob.objfun(pop.champion.x)))
    print("Decomposed fitness: {}\n".format(pop.champion.f))