Single Objective Problems: Rastrigin

Preamble

In [1]:
# used to create block diagrams
%reload_ext xdiag_magic
%xdiag_output_format svg
    
import numpy as np                   # for multi-dimensional containers
import pandas as pd                  # for DataFrames
import plotly.graph_objects as go    # for data visualisation
import plotly.io as pio              # to set shahin plot layout

pio.templates['shahin'] = pio.to_templated(go.Figure().update_layout(margin=dict(t=0,r=0,b=40,l=40))).layout.template
pio.templates.default = 'shahin'

Introduction

Before moving on, let's take some time to have a closer look at a single-objective problem. This will give us some perspective. In single-objective problems, the objective is to find a single solution which represents the global optimum in the entire search space. Determining which solutions outperforms others is a simple task when only considering a single-objective, because the best solution is simply the one with the highest (for maximisation problems) or lowest (for minimisation problems) objective value. Let's take the Rastrigin function as an example.

The Rastrigin Function

This is a single-objective test function which has been expressed in Equation 1.

$$ f(\mathbf{x}) = A n + \sum_{d=1}^D \left[x_i^2 - A\cos(2 \pi x_d)\right]\tag{1} $$

where $x_d \in [-5.12, 5.12]$, i.e. each problem variable should be between $-5.12$ and $5.12$. It is continuous, convex, and unimodal. It is scalable with regards to its dimensionality in the problem domain, however we will use its two-dimensional configuration for easy visualisation throughout this notebook. Let's implement this problem in Python so we can evaluate some solutions.

In [2]:
def rastrigin(x):
    A = 10
    y = A * D + np.sum(np.square(x) - A * np.cos(2 * np.pi * x))
    return y

Now let's prepare for the initialisation of five solutions with two problem variables. We will specify the desired population size, $\mathrm{N}$, and the dimensionality of the problem domain, $\mathrm{D}$. We will also specify the lower and upper boundaries for our problem variables.

In [3]:
N = 5
D = 2
D_lower = np.ones((1, D)) * -5.12
D_upper = np.ones((1, D)) * 5.12

Let's initialise a population with random decision variable

In [4]:
X = pd.DataFrame(np.random.uniform(low=D_lower, high=D_upper, size=(N,D)))
X
Out[4]:
0 1
0 -0.771338 -2.599701
1 3.158239 2.268091
2 4.114275 2.784494
3 0.482772 4.519134
4 4.866421 2.160641

To generate $\mathrm{Y}$, we will evaluate each solution $\mathrm{X}_{\mathrm{N}}$ using the rastrigin() test function defined above.

In [5]:
Y = np.empty((0, 1))

X = pd.DataFrame(np.random.uniform(low=D_lower, high=D_upper, size=(N,D)))

for n in range(N):
    y = rastrigin(X.iloc[n])
    Y = np.vstack([Y, y])
    
# convert to DataFrame
Y = pd.DataFrame(Y, columns=['f'])

We only have five solutions, so it's feasible to list all our objective values.

In [6]:
Y
Out[6]:
f
0 21.269135
1 37.157999
2 23.898303
3 70.830575
4 58.584472

We can very easily select the best solution from the above population. It is simply the solution with the smallest objective value (as we are concerned with minimisation).

In [7]:
np.min(Y)
Out[7]:
f    21.269135
dtype: float64

Note

The above example relies on random numbers. This means you will see different results upon executing the code in this notebook for yourself.

Let's see if we can get a better understanding of the problem domain by generating and visualising 3000 more solutions. This means setting $\mathrm{N}=3000$. We can achieve this by repeating the same code as above with slightly different parameters to generate our population of solutions, and then evaluate them using our rastrigin() function.

In [8]:
N = 3000

X = pd.DataFrame(
    np.random.uniform(low=D_lower, high=D_upper, size=(N,D)),
    columns=['x1', 'x2']
)

Y = np.empty((0, 1))

for n in range(N):
    y = rastrigin(X.iloc[n])
    Y = np.vstack([Y, y])
    
Y = pd.DataFrame(Y, columns=['f'])

All that's left now is to visualise the results. For this, we'll use a 3D scatterplot to plot the problem variables of each solution along with each corresponding objective value. We will reserve the vertical axis for the objective value, $f$, meaning that "lower" points represent better performing solutions.

In [9]:
fig = go.Figure(layout=dict(scene = dict(xaxis_title='x1',
                                         yaxis_title='x2',
                                         zaxis_title='f')))

fig.add_scatter3d(x=X.x1, y=X.x2, z=Y.f, 
                  mode='markers',
                  marker=dict(color = -Y.f, size=4))

fig.show()