最近发现jMetal框架更新到了5.2,我也把以前写代码顺着框架重新改动一遍,正好整理出来供大家参考。
jMetal是Java实现的一套多目标优化框架,只需要很少量的改动就能定制自己的算法。关键是开源包里包括了几乎所有常用的算法,大大方便了懒人群体。
关于项目的其他信息,请移步:https://jmetal.github.io/jMetal/,github:https://github.com/jMetal/jMetal
下载使用
jMetal的5.0版本以后终于maven化了
maven依赖:
org.uma.jmetal
jmetal-core
5.2
org.uma.jmetal
jmetal-algorithm
5.2
org.uma.jmetal
jmetal-problem
5.2
org.uma.jmetal
jmetal-exec
5.2
如果不使用maven,可以去官网下载对应的jar包添加到工程里面。
jmetal-core是核心包,jmetal-exec是算法配置包。jmetal-algorithm是各类算法的具体实现,而jmetal-problem是各类优化问题包。其中,jmetal-core是必须的,其余三个包视情况而定,建议都添加。
下面先对jMetal的基本框架和主要的类做一个介绍,下一节再贴出一个完整的Demo。
算法框架
jmetal-core框架
The Algorithm :
Algorithm是所有优化算法的模板,只定义了两个接口方法run()和getResult()。run()是算法的运行入口,getResult()用于返回结果集,一般就是算法得到的Pareto集。代码如下:
package org.uma.jmetal.algorithm;
/**
* Interface representing an algorithm
* @author Antonio J. Nebro
* @version 0.1
* @param Result
*/
public interface Algorithm extends Runnable {
void run() ;
Result getResult() ;
}
你的自己的算法需要继承的是 AbstractGeneticAlgorithm这个类。作者的意图是写一个多目标优化的通用框架,遗传算法只是其中之一,而AbstractGeneticAlgorithm就是遗传算法的算法模板。
这个AbstractEvolutionaryAlgorithm的代码如下:
public abstract class AbstractGeneticAlgorithm extends AbstractEvolutionaryAlgorithm {
protected int maxPopulationSize ;
protected SelectionOperator, S> selectionOperator ;
protected CrossoverOperator crossoverOperator ;
protected MutationOperator mutationOperator ;
...
/**
* Constructor
* @param problem The problem to solve
*/
public AbstractGeneticAlgorithm(Problem problem) {
setProblem(problem);
}
/**
* This method implements a default scheme create the initial population of genetic algorithm
* @return
*/
protected List createInitialPopulation() {
List population = new ArrayList<>(getMaxPopulationSize());
for (int i = 0; i < getMaxPopulationSize(); i++) {
S newIndividual = getProblem().createSolution();
population.add(newIndividual);
}
return population;
}
/**
* This method iteratively applies a {@link SelectionOperator} to the population to fill the mating pool population.
*
* @param population
* @return The mating pool population
*/
@Override
protected List selection(List population) {
List matingPopulation = new ArrayList<>(population.size());
for (int i = 0; i < getMaxPopulationSize(); i++) {
S solution = selectionOperator.execute(population);
matingPopulation.add(solution);
}
return matingPopulation;
}
/**
* This methods iteratively applies a {@link CrossoverOperator} a {@link MutationOperator} to the population to
* create the offspring population. The population size must be divisible by the number of parents required
* by the {@link CrossoverOperator}; this way, the needed parents are taken sequentially from the population.
*
* No limits are imposed to the number of solutions returned by the {@link CrossoverOperator}.
*
* @param population
* @return The new created offspring population
*/
@Override
protected List reproduction(List population) {
int numberOfParents = crossoverOperator.getNumberOfParents() ;
checkNumberOfParents(population, numberOfParents);
List offspringPopulation = new ArrayList<>(getMaxPopulationSize());
for (int i = 0; i < getMaxPopulationSize(); i += numberOfParents) {
List parents = new ArrayList<>(numberOfParents);
for (int j = 0; j < numberOfParents; j++) {
parents.add(population.get(i+j));
}
List offspring = crossoverOperator.execute(parents);
for(S s: offspring){
mutationOperator.execute(s);
offspringPopulation.add(s);
}
}
return offspringPopulation;
}
...
}
run()函数内部就是整个遗传算法的流程了,一目了然。各个具体的遗传算法只需要继承这个类,对很少的函数进行override就得到了一个完整的算法。
比如官方NSGA-II:
public class NSGAII> extends AbstractGeneticAlgorithm> {
...
/**
* Constructor
*/
public NSGAII(Problem problem, int maxEvaluations, int populationSize,
CrossoverOperator crossoverOperator, MutationOperator mutationOperator,
SelectionOperator, S> selectionOperator, SolutionListEvaluator evaluator) {
super(problem);
this.maxEvaluations = maxEvaluations;
setMaxPopulationSize(populationSize); ;
this.crossoverOperator = crossoverOperator;
this.mutationOperator = mutationOperator;
this.selectionOperator = selectionOperator;
this.evaluator = evaluator;
}
...
@Override protected List replacement(List population, List offspringPopulation) {
List jointPopulation = new ArrayList<>();
jointPopulation.addAll(population);
jointPopulation.addAll(offspringPopulation);
RankingAndCrowdingSelection rankingAndCrowdingSelection ;
rankingAndCrowdingSelection = new RankingAndCrowdingSelection(getMaxPopulationSize()) ;
return rankingAndCrowdingSelection.execute(jointPopulation) ;
}
...
}
在构造函数里,你需要提供算法的其他组件(不用担心,这些组件,包里都有现成的):
- Problem:要解决的问题类
- CrossoverOperator:交叉算子
- MutationOperator:变异算子
- SelectionOperator:matepool的选择算子
- Evaluator:优化函数评估算子
- populationSIze:种群大小
- maxEvations:最大评估次数(作为停止条件)
AbstractGeneticAlgorithm会在对应的函数中调用这些组件,比如reproduction()会调用crossoverOperator、CrossoverOperator和MutationOperator三个组件,如果你需要自己定义选择、交叉、变异这些操作的话,可以实现对应的接口,然后将这些类作为组件提供给具体的Algorithm。
The Solution:
注意构造函数中的泛型参数S extends Soluiton。Solution是个体定义的接口,每个Solution就是种群中一个个体,Solution需要做的是定义染色体编码方案,记录优化函数值,约束目标值,其他信息等,默认提供了BinarySoluiton、DoubleSolution、IntegerSoluiton等。同样的,如果需要定义新的Solution,你需要继承的是AbstractGenericSolution这个类。
public abstract class AbstractGenericSolution> implements Solution {
private double[] objectives;
private List variables;
protected P problem ;
protected double overallConstraintViolationDegree ;
protected int numberOfViolatedConstraints ;
protected Map
The Problem:
构造函数中稍微难理解一点的就是这个Problem,problem定义了需要解决的问题,比如有几个决策变量,几个优化函数。其中每个个体的优化函数的计算过程就是由Problem.evluate()来实现的,这也是Problem类最重要的部分。举个例子:ZDT1,ZDT是一个经典的多目标优化测试函数集,定义如下:
(两个优化函数,30个决策变量)
jMetal中ZDT1的代码如下:
public class ZDT1 extends AbstractDoubleProblem {
/** Constructor. Creates default instance of problem ZDT1 (30 decision variables) */
public ZDT1() {
this(30);
}
/**
* Creates a new instance of problem ZDT1.
*
* @param numberOfVariables Number of variables.
*/
public ZDT1(Integer numberOfVariables) {
setNumberOfVariables(numberOfVariables);
setNumberOfObjectives(2);
setName("ZDT1");
List lowerLimit = new ArrayList<>(getNumberOfVariables()) ;
List upperLimit = new ArrayList<>(getNumberOfVariables()) ;
for (int i = 0; i < getNumberOfVariables(); i++) {
lowerLimit.add(0.0);
upperLimit.add(1.0);
}
setLowerLimit(lowerLimit);
setUpperLimit(upperLimit);
}
/** Evaluate() method */
public void evaluate(DoubleSolution solution) {
double[] f = new double[getNumberOfObjectives()];
f[0] = solution.getVariableValue(0);
double g = this.evalG(solution);
double h = this.evalH(f[0], g);
f[1] = h * g;
solution.setObjective(0, f[0]);
solution.setObjective(1, f[1]);
}
/**
* Returns the value of the ZDT1 function G.
*
* @param solution Solution
*/
private double evalG(DoubleSolution solution) {
double g = 0.0;
for (int i = 1; i < solution.getNumberOfVariables(); i++) {
g += solution.getVariableValue(i);
}
double constant = 9.0 / (solution.getNumberOfVariables() - 1);
g = constant * g;
g = g + 1.0;
return g;
}
/**
* Returns the value of the ZDT1 function H.
*
* @param f First argument of the function H.
* @param g Second argument of the function H.
*/
public double evalH(double f, double g) {
double h ;
h = 1.0 - Math.sqrt(f / g);
return h;
}
}
代码很简单。其中AbstractDoubleProblem 是通用的DoubleProblem模板,如果你的问题是Double类型的实现这类就可以了。evaluate()由AbstractEvolutionaryAlgorithm.evaluatePopulation()调用,在你的算法中,如果需要自己定义优化函数,就可以在evaluate()中进行定义。
The Operator:
交叉、变异和选择算子都是Operator接口的实现,这里用SimpleRandomMutation这个类来说明,SimpleRandomMutation实现变量在定义域内变异。
public class SimpleRandomMutation implements MutationOperator {
private double mutationProbability ;
private RandomGenerator randomGenerator ;
...
/** Execute() method */
@Override
public DoubleSolution execute(DoubleSolution solution) throws JMetalException {
if (null == solution) {
throw new JMetalException("Null parameter") ;
}
doMutation(mutationProbability, solution) ;
return solution;
}
/** Implements the mutation operation */
private void doMutation(double probability, DoubleSolution solution) {
for (int i = 0; i < solution.getNumberOfVariables(); i++) {
if (randomGenerator.getRandomValue() <= probability) {
Double value = solution.getLowerBound(i) +
((solution.getUpperBound(i) - solution.getLowerBound(i)) * randomGenerator.getRandomValue()) ;
solution.setVariableValue(i, value) ;
}
}
}
}
逻辑非常简单,不过注意在DoubleSolution里面设定好决策变量的定义域大小,如果每个决策变量的定义域不一样的话可以用ArrayDoubleSolution(extends DoubleSolution)这个类。
交叉选择算子和变异算子很类似,可以自己看包里的源码,这里就不做说明了。
结果集
算法运行完后,是通过调用Algorithem.getResult()来得到结果集的,对大部分算法来说也就是Pareto集。NSGA-II中的实现代码如下:
@Override public List getResult() {
return getNonDominatedSolutions(getPopulation());
}
protected List getNonDominatedSolutions(List solutionList) {
return SolutionListUtils.getNondominatedSolutions(solutionList);
}
SolutionListUtils类提供了对种群的操作,如对种群按支配关系排序,得到最好最差解等。和4.0版本采用SolutionSet不同的是5.0版本以后采用的是List
以上就是整个框架的大致组成了,总结一下:
- 实现AbstractGenericSolution,在具体类里面定义编码方案,定义决策变量,定义优化目标函数储存方法;
- 实现AbstractDoubleProblem,在具体类中定义需要解决的问题,overide evaluate()来定义优化函数;
- 实现各个Operator:选择、交叉、变异;
- 实现AbstractEvolutionaryAlgorithm,Operator提供给具体的Algorithm类,并设定参数;
- 调用run()跑算法,调用getResult()得到结果集。
上述步骤的具体实现在包里几乎都有,所以应用起来会非常简单。
下一节给出NSGA-II的一个具体例子,免得还是云里雾里的。