NSGA3 Algorithm

[15]:
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from mpl_toolkits import mplot3d

Reference directions

For demonstration purposes, shown are the reference directions used by the NSGA3 algorithm to maintain a diverse non-dominated front. These vectors are generated automatically when instantiating the NSGA3 algorithm. When selecting individuals for the next generation, NSGA3 finds solutions that are closest to under-represented reference directions.

[16]:
from gatf.population import uniform_reference_points
[17]:
ref_points = uniform_reference_points(3,10)

fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
ax.scatter(ref_points[:,0], ref_points[:,1], ref_points[:,2])
ax.view_init(30, 25)
../_images/notebooks_NSGA3_4_0.png

ZDT2 function

Define the objective

Our first objective is the ZDT2 function. This is a 30-dimensional problem with a non-convex pareto front.

[18]:
@tf.function(input_signature=(tf.TensorSpec(shape=[None,None], dtype=tf.float64),))
def ZDT2(X):
    g = tf.reshape(1.0 + (9.0/(tf.cast(tf.shape(X)[1],tf.float64) - 1.0)) * tf.reduce_sum(X[:,1:], axis=1), (-1,1))
    f1 = tf.reshape(X[:,0], (-1,1))
    f2 = g * (1.0 - (f1 / g)**2)
    return tf.concat([f1,f2],axis=1)

Perform the optimization

Here we’ll begin the optimizer. Hyperparameters such as the population size, generations, crossover and mutation probabilities may need to be tuned to your particular application.

[19]:
from gatf import NSGA3
[20]:
# Instantiate the optimizer
nsga = NSGA3(ZDT2,
             n_variables = 30,
             n_objectives = 2,
             population_size = 92,
             tournament_size = 5,
             generations = 3000,
             lowerbound = 0.0,
             upperbound = 1.0,
             mutation_prob = 0.1,
             crossover_prob = 0.9,
             record_gens = 50,
             mutation_eta = 30.0,
             crossover_eta = 10.0
            )
Population size: 92, reference directions: 88, partitions: 87
[21]:
# Go!
res = nsga()
Generation 3000 – MDR 0.04 – Ref dirs 64.0%
Complete – non-dominated front size: 92

Plotting results

Let’s plot the final population distributed on the pareto front of the function.

[22]:
# Get results to plot
F = res['fitness']

fig,ax = plt.subplots()
ax.scatter(F[:,0],F[:,1])
xlim,ylim = ax.get_xlim(),ax.get_ylim()
ax.set_xlim(xlim)
ax.set_ylim(ylim)
[22]:
(-0.03400067670650153, 1.0508955829016213)
../_images/notebooks_NSGA3_12_1.png

Plotting statistics

Now let’s look at some statistics. The mutual domination rate compares the current with the former generation. Values greater than one indicate an improvement while values less than one indicate that the current population at each generation is partially dominated by the previous. If zero, then there is no improvement, suggesting that the true pareto front has been reached. The % filled reference directions give an indication of the diversity of the solutions, with larger values indicating greater diversity.

[23]:
# Get results to plot
stats = res['statistics']

gen = np.array(stats['gen'])
mdr = np.array(stats['mdr'])
refs = 100 * np.array(stats['refs'])

fig,ax = plt.subplots(1,2, figsize=[12,4])

ax[0].plot(gen, mdr, label = 'mdr')
ax[0].set_xlabel('gen')
ax[0].set_ylabel('MDR')

ax[1].plot(gen, refs, label = 'filled references')
ax[1].set_xlabel('gen')
ax[1].set_ylabel('% filled reference directions')

plt.subplots_adjust(wspace = 0.3)
../_images/notebooks_NSGA3_14_0.png

ZDT3 function

Define the objective

Now let’s try the ZDT3 function. This is a 30-dimensional problem with multiple disconnected pareto fronts.

[24]:
@tf.function(input_signature=(tf.TensorSpec(shape=[None,None], dtype=tf.float64),))
def ZDT3(X):
    g = tf.reshape(1.0 + (9.0/(tf.cast(tf.shape(X)[1],tf.float64) - 1.0)) * tf.reduce_sum(X[:,1:], axis=1), (-1,1))
    f1 = tf.reshape(X[:,0], (-1,1))
    h = tf.reshape(1.0 - tf.sqrt(f1 / g) - (f1 / g) * tf.math.sin(10.0 * np.pi * f1), (-1,1))
    f2 = g * h
    return tf.concat([f1,f2],axis=1)

x,y = np.meshgrid(np.linspace(0,1,100),np.linspace(0,1,100))
x = x.reshape((-1,1))
y = y.reshape((-1,1))
P = ZDT3(np.hstack([x,y]))

Perform the optimization

Here we’ll begin the optimizer. Hyperparameters such as the population size, generations, crossover and mutation probabilities may need to be tuned to your particular application.

[25]:
# Instantiate the optimizer
nsga = NSGA3(ZDT3,
             n_variables = 30,
             n_objectives = 2,
             population_size = 92,
             tournament_size = 5,
             generations = 3000,
             lowerbound = 0.0,
             upperbound = 1.0,
             mutation_prob = 0.3,
             crossover_prob = 0.7,
             record_gens = 50,
             mutation_eta = 30.0,
             crossover_eta = 30.0
            )
Population size: 92, reference directions: 88, partitions: 87
[26]:
# Go!
res = nsga()
Generation 3000 – MDR 0.07 – Ref dirs 42.0%
Complete – non-dominated front size: 92

Plotting results

Let’s plot the final population distributed on the pareto front of the function.

[27]:
# Get results to plot
F = res['fitness']

fig,ax = plt.subplots()
ax.scatter(F[:,0],F[:,1])
xlim,ylim = ax.get_xlim(),ax.get_ylim()
ax.scatter(P[:,0],P[:,1], c='k', s=0.2, alpha = 0.5)
ax.set_xlim(xlim)
ax.set_ylim(ylim)
[27]:
(-0.8226924820149453, 0.95989684364452)
../_images/notebooks_NSGA3_21_1.png

Plotting statistics

And now some statistics. The positive MDR throughout suggests that continuous improvements were being made. Running the algorithm for more generations or tuning the hyperparameters may give a better result.

[28]:
# Get results to plot
stats = res['statistics']

gen = np.array(stats['gen'])
mdr = np.array(stats['mdr'])
refs = 100 * np.array(stats['refs'])

fig,ax = plt.subplots(1,2, figsize=[12,4])

ax[0].plot(gen, mdr, label = 'mdr')
ax[0].set_xlabel('gen')
ax[0].set_ylabel('MDR')

ax[1].plot(gen, refs, label = 'filled references')
ax[1].set_xlabel('gen')
ax[1].set_ylabel('% filled reference directions')

plt.subplots_adjust(wspace = 0.3)
../_images/notebooks_NSGA3_23_0.png
[ ]: