MDCHAMP / FreeLunch

Meta-heuristic optimisation suite for python
https://pypi.org/project/freelunch/
MIT License
46 stars 3 forks source link
heuristic-search-algorithms meta-heuristic-algorithms optimisation-algorithms python

FreeLunch - Meta-heuristic optimisation suite for python

PyPICode Tests Benchmark Coverage

Please note the minor changes to the optimiser call signature since 0.0.11, details below.


About

Freelunch is a convenient python implementation of a number of meta-heuristic optimisation (with an 's') algorithms.


Features

Optimisers

--NEW and EXCITING---

Your favourite not in the list? Feel free to add it.

--Coming soon to 0.1.0--

Benchmarking functions

Tier list: TBA


Install

Install with pip (req. numpy).

pip install freelunch

Usage

Create instances of your favourite meta-heuristics!

import freelunch
opt = freelunch.DE(my_objective_function, bounds=my_bounds) # Differential evolution

Where,

Running the optimisation

Run by calling the instance. There are several different calling signatures. Use any combination of the arguments below to suit your needs!

To return the best solution only:

quick_result = opt() # (D,)

To return optimum after n_runs:

best_of_nruns = opt(n_runs=n) # (D,)

To return optimum after n_runs in parallel (uses multiprocessing.Pool), see note below.:

best_of_nruns = opt(n_runs=n, n_workers=w, pool_args={}, chunks=1) # (D,)

Return best m solutions in np.ndarray:

best_m = opt(n_return=m) # (D, m)

Return json friendly dict with fun metadata!

full_output = opt(full_output=True)
    # {
    #     'optimiser':'DE',
    #     'hypers':...,
    #     'bounds':...,
    #     'nruns':nruns,
    #     'nfe':1234,
    #     'solutions':[sol1, sol2, ..., solm*n_runs], # All solutions from all runs sorted by fitness
    #     'scores':[fit1, fit2, ..., fitm*n_runs]
    # }

Customisation

Want to change things around?

Custom initialisation strategies can be supplied by altering the optimiser.initialiser attribute of any optimiser instance. For example:

opt = fr.DE(obj, ...)

def my_initialiser(bounds, N, **hypers):
    'Custom population initialisation'
    # Remember to return and interable of length N
    population = ... # custum logic
    return population

Additionally, some examples of common initialisation strategies can be found in the freelunch.tech module.

The simplest way to do this is to overwrite the optimiser.bounder attribute. There are a number of ready made strategies in freelunch.tech or alternatively define a custom method with the following call signature.


opt = fr.DE(obj, ...)

def my_bounder(p, bounds, **hypers):
    '''custom bounding method'''
    p.dna = ... # custom bounding logic

opt.bounder = my_bounder # overwrite the bounder attribute

# and then call as before
x_optimised = opt()

Check out the hyperparameters and set your own, (defaults set automatically):

print(opt.hyper_definitions)
    # {
    #     'N':'Population size (int)',
    #     'G':'Number of generations (int)',
    #     'F':'Mutation parameter (float in [0,1])',
    #     'Cr':'Crossover probability (float in [0,1])'
    # }

print(opt.hyper_defaults)
    # {
    #     'N':100,
    #     'G':100,
    #     'F':0.5,
    #     'Cr':0.2
    # }

opt.hypers.update({'N':300})
print(opt.hypers)
    # {
    #     'N':300,
    #     'G':100,
    #     'F':0.5,
    #     'Cr':0.2
    # }

Benchmarks

Access from freelunch.benchmarks for example:

bench = freelunch.benchmarks.ackley(n=2) # Instanciate a 2D ackley benchmark function

fit = bench(sol) # evaluate by calling
bench.bounds # [[-10, 10],[-10, 10]]
bench.optimum # [0, 0] 
bench.f0 # 0.0

A note on running optimisations in parallel.

Because multiprocessing.Pool relies on multiprocessing.forking.pickle to send code to parallel processes, it is imperative that anything passed to the freelunch optimisers can be pickled. For example, the following common python pattern for producing an objective function with a single argument,


method = ... # some methods / args that are requred by the objective function
args = 

def wrap_my_obj(method, args):
    def _obj(x):
        return method(args, x)
    return _obj

obj = wrap_my_obj(method, args)

cannot be pickled because _obj is not importable from the top level module scope and will raise freelunch.util.UnpicklableObjectiveFunction . Instead consider using functools.partial i.e.


from functools import partial

method = ... # some methods / args that are requred by the objective function
args = ...

def _obj(method, args, x):
    return method(args, x)

obj = partial(_obj, method, args)