Skip to content

gsoleilhac/NSGAII.jl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Build Status Build status

codecov.io Coverage Status

References

A Fast Elitist Non-Dominated Sorting Genetic Algorithm for Multi-Objective Optimization: NSGA-II Kalyanmoy Deb, Samir Agrawal, Amrit Pratap, and T Meyarivan

Installation

julia> ]
pkg> add NSGAII

Usage

Example : Bi-Objective Knapsack

using NSGAII
n = 20 #Number of items
p1 = [77,94,71,63,96,82,85,75,72,91,99,63,84,87,79,94,90,60,69,62] #Coeffs objective 1
p2 = [65,90,90,77,95,84,70,94,66,92,74,97,60,60,65,97,93,60,69,74] #Coeffs objective 2
w = [80,87,68,72,66,77,99,85,70,93,98,72,100,89,67,86,91,79,71,99] #Items weights
c = 900 #Knapsack capacity

The four mandatory parameters of NSGAII are

  • the size of the population
  • the number of generations
  • an initialization function
  • an evaluation function
using Random: bitrand
using LinearAlgebra: dot

popsize = 100
nbgen = 200
init() = bitrand(n) #our genotype is a binary vector of size n, initialized randomly
z(x) = dot(x, p1), dot(x, p2) #and our objectives are the sum of the items we pick

Now, this would be enough to run nsga-2 with nsga_max(popsize, nbgen, z, init)
But we need to add the constraint that all items must fit in the knapsack.
For this we define a constraint-violation function that returns 0 only if the solution is feasible,
and a value > 0 otherwise.

function CV(x)
    sumW = dot(x, w)
    return sumW <= c ? 0 : sumW - c
end

#We can now call
result = nsga_max(popsize, nbgen, z, init, fCV = CV)

result will be a vector of individuals.
The revelant fields of an individual indiv are :

  • genotype : indiv.x
  • objective values : indiv.y
  • rank : indiv.rank
  • constraint violation value : indiv.CV

Crossover

If the solutions are encoded as bitstrings, a 2-point crossover will be used by default, but we can define our own and assign it with the keyword fcross:

function one_point_crossover!(parent_a, parent_b, child_a, child_b)
    n = length(parent_a)
    cut = rand(1:n-1)

    child_a[1:cut] .= parent_a[1:cut]
    child_a[cut+1:n] .= parent_b[cut+1:n]

    child_b[1:cut] .= parent_b[1:cut]
    child_b[cut+1:n] .= parent_a[cut+1:n]
end

nsga_max(popsize, nbgen, z, init, fCV = CV, fcross = one_point_crossover!)

For permutations genotypes, the default crossover is the PMX (Partially-Mapped Crossover)

Mutation

The default mutation for a binary vector is the bitstring mutation where each bit has a probability 1/l to be flipped (where l is the length of the vector)

As with crossovers, we can define or own mutation operator and assign it with the keyword fmut. The probability of mutation can be changed with the keyword pmut.

Let's say we want our mutation to flip two random bits :

function two_bits_flip!(bits)
    for i = 1:2
        n = rand(1:length(bits))
        bits[n] = 1 - bits[n]
    end
end

nsga_max(popsize, nbgen, z, init, fCV = CV, fmut = two_bits_flip!, pmut = 0.2)

For permutations genotypes, the default mutation randomly swaps two indices.

Seeding

Starting solutions can be provided as a vector with the keyword seed, for example :

x1 = greedy(p1, w, c)
x2 = greedy(p2, w, c)
x3 = greedy(p1 .+ p2, w, c)

nsga_max(popsize, nbgen, z, init, fCV = CV, seed = [x1, x2, x3])

Make sure the type of your seeds is the same as the one given by calling init() !

Plotting

A plot function can be passed with the keyword fplot, by default the population is plotted at every generation but this can be changed with the keyword plotevery.

Example with PyPlot :

using PyPlot

function plot_pop(P)
    clf() #clears the figure
    P = filter(indiv -> indiv.rank == 1, P) #keep only the non-dominated solutions
    plot(map(x -> x.y[1], P), map(x -> x.y[2], P), "bo", markersize = 1)
    sleep(0.1)
end

nsga_max(popsize, nbgen, z, init, fCV = CV, fplot = plot_pop, plotevery = 5)

BinaryCoding

You can use BinaryCoding(ϵ::Int, lb::Vector, ub::Vector) to encode real variables with a precision ϵ, and with lower and upper bounds lb and ub

Example : MOP7 from Van Valedhuizen’s Test Suite

MOP7

using NSGAII
using Plots: scatter3d

f1(x1,x2) = ((x1-2)^2)/2 + ((x2+1)^2)/13 + 3
f2(x1,x2) = ((x1+x2-3)^2)/36 + ((-x1+x2+2)^2)/8 - 17
f3(x1,x2) = ((x1+2x2-1)^2)/175 + ((-x1+2x2)^2)/17 - 13

z(x) = f1(x[1], x[2]), f2(x[1], x[2]), f3(x[1], x[2])

#Encodes two variables -400 <= x_i <= 400, with a precision of 1E-4
const bc = BinaryCoding(4, [-400, -400], [400, 400]) 

function plot_pop(pop)
    pop = filter(indiv -> indiv.rank <= 1, pop) #keeps only the non-dominated solutions
    scatter3d(map(x -> x.y[1], pop), map(x -> x.y[2], pop),  map(x -> x.y[3], pop), markersize = 1) |> display
    sleep(0.1)
end

nsga(500, 100, z, bc, seed = [[1.,-1.],[2.5,0.5],[0.5,0.25]], fplot = plot_pop)

  • The initialization function isn't needed anymore.
  • The seed is passed as a vector of phenotypes, not a vector of genotypes, it is automatically encoded.

You can also use BinaryCoding(ϵ::Int, types, lb, ub) to encode a mix of integer, continuous or binary variables, with types a vector of symbols : ( :Int | :Cont | :Bin ).

Misc

The progress bar can be disabled by calling nsga(..., showprogress = false)