Test Functions

Before diving into specific optimization algorithms, we will first take a look at some test functions which can help us check how well our algorithms are performing. There are lots of Test functions for optimization out there, serving different purposes. Because we want to visualize the test functions, we will only consider functions on \(\mathbb{R}^2\).

Base Class

Before starting with any concrete functions, we will first define an abstract class ObjectiveFunction which will be the base class for all test functions.

import numpy as np
from abc import ABC, abstractmethod
from matplotlib.colors import Normalize
class ObjectiveFunction(ABC):

    def __init__(self, *, args=()):
        self.args = args

    def __call__(self, p, *, deriv=0):
        if deriv == 0:
            return self._get_value(p, self.args)
        elif deriv == 1:
            return self._get_gradient(p, self.args)
        else:
            raise ValueError('Only 0 or 1 allowed for deriv!')

    @abstractmethod
    def _get_value(self, p, args=()):
        pass

    @abstractmethod
    def _get_gradient(self, p, args=()):
        pass

This abstract class defines the __call__ method, which will be executed when we call an instance of a class derived from ObjectiveFunction. Because the function value and gradient depend on the specific function, they are decorated with the @abstractmethod decorator. This means that any class derived from ObjectiveFunction must implement these two methods.

We may also want to plot the function and possibly the optimization path. We can define two functions for this purpose.

def plot_2d_objective_function(ax, func, xs, ys, norm=None):
    p_grid = np.meshgrid(xs, ys)
    values = func(p_grid, deriv=0)
    
    dx = (xs.max() - xs.min()) / (len(xs) - 1.0)
    dy = (ys.max() - ys.min()) / (len(ys) - 1.0)
    extent = [xs.min() - 0.5*dx, xs.max() + 0.5*dx, ys.min() - 0.5*dy, ys.max() + 0.5*dy]
    
    ax.set_xlabel('$x_1$')
    ax.set_ylabel('$x_2$')
    
    if norm is None:
        normalise = Normalize(vmin=values.min(), vmax=values.max())
    else:
        normalise = norm
        normalise.vmin = values.min()
        normalise.vmax = values.max()
    
    im = ax.imshow(values, origin='lower', aspect='auto', extent=extent, norm=normalise)
    ax.get_figure().colorbar(im, ax=ax)
def plot_2d_optimisation(ax, func, xs, ys, norm=None, traj=None):
    plot_2d_objective_function(ax, func, xs, ys, norm=norm)
    if traj is not None:        
        xlim = ax.get_xlim()
        ylim = ax.get_ylim()
        ax.plot(traj[:, 0], traj[:, 1], c='w', marker='o')
        ax.set_xlim(xlim)
        ax.set_ylim(ylim)

Himmelblau's Function

Himmelblau's function is a rational function defined by $$ f(x) = (x_1^2 + x_2 - 11)^2 + (x_1 + x_2^2 - 7)^2 $$

It has 4 local minima, all with the same function value \(f(x^{*}) = 0\).

class HimmelblauFunction(ObjectiveFunction):

    def _get_value(self, p, args):
        x, y = p
        return (x**2 + y - 11.0)**2 + (x + y**2 - 7)**2

    def _get_gradient(self, p, args):
        x, y = p
        fx = 4.0 * x * (x**2 + y - 11.0) + 2.0 * (x + y**2 - 7)
        fy = 2.0 * (x**2 + y - 11.0) + 4.0 * y * (x + y**2 - 7)
        return np.array([fx, fy])

Rosenbrock's Function

Rosenbrock's function is a non-convex function defined by $$ f(x) = (a - x_1)^2 + b(x_2 - x_1^2)^2 $$ where \(a\) and \(b\) are parameters. The function has a global minimum at \(x = (a, a^2)\) with \(f(x^{*}) = 0\). This global minimum lies in a long, narrow and relatively flat valley, which makes it difficult to find.

class RosenbrockFunction(ObjectiveFunction):

    def _get_value(self, p, args):
        x, y = p
        a, b = args
        return (a - x)**2 + b * (y - x**2)**2

    def _get_gradient(self, p, args):
        x, y = p
        a, b = args
        fx = 2.0 * (x - a) + 4.0 * b * x * (x**2 - y)
        fy = 2.0 * b * (y - x**2)
        return np.array([fx, fy])

We can use our routine plot_2d_objective_function to plot these two functions:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
from objective_function import RosenbrockFunction, HimmelblauFunction, \
    plot_2d_objective_function

norm = LogNorm()

hf = HimmelblauFunction()
xs1 = np.linspace(-6.0, 6.0, 200)
ys1 = np.linspace(-6.0, 6.0, 200)

rf = RosenbrockFunction(args=(1.0, 100.0))
xs2 = np.linspace(-2.0, 2.0, 200)
ys2 = np.linspace(-1.0, 3.0, 200)

fig, axs = plt.subplots(1, 2, figsize=(10, 4))

axs[0].set_title('Himmelblau\'s function')
plot_2d_objective_function(axs[0], hf, xs1, ys1, norm=norm)

axs[1].set_title('Rosenbrock function ($a = 1;\ b = 100$)')
plot_2d_objective_function(axs[1], rf, xs2, ys2, norm=norm)

fig.tight_layout()
plt.show()