转自:https://my.oschina.net/u/3534184/blog/918928
最近发现jMetal框架更新到了5.2,我也把以前写代码顺着框架重新改动一遍,正好整理出来供大家参考。
jMetal是Java实现的一套多目标优化框架,只需要很少量的改动就能定制自己的算法。关键是开源包里包括了几乎所有常用的算法,大大方便了懒人群体。
关于项目的其他信息,请移步:https://jmetal.github.io/jMetal/,github:https://github.com/jMetal/jMetal
下载使用
jMetal的5.0版本以后终于maven化了
maven依赖:
<!-- https://mvnrepository.com/artifact/org.uma.jmetal/jmetal-core -->
<dependency>
<groupId>org.uma.jmetal</groupId>
<artifactId>jmetal-core</artifactId>
<version>5.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.uma.jmetal/jmetal-algorithm -->
<dependency>
<groupId>org.uma.jmetal</groupId>
<artifactId>jmetal-algorithm</artifactId>
<version>5.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.uma.jmetal/jmetal-problem -->
<dependency>
<groupId>org.uma.jmetal</groupId>
<artifactId>jmetal-problem</artifactId>
<version>5.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.uma.jmetal/jmetal-exec -->
<dependency>
<groupId>org.uma.jmetal</groupId>
<artifactId>jmetal-exec</artifactId>
<version>5.2</version>
</dependency>
如果不使用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> Result */ public interface Algorithm<Result> extends Runnable { void run() ; Result getResult() ; }
你的自己的算法需要继承的是 AbstractGeneticAlgorithm这个类。作者的意图是写一个多目标优化的通用框架,遗传算法只是其中之一,而AbstractGeneticAlgorithm就是遗传算法的算法模板。
这个AbstractEvolutionaryAlgorithm的代码如下:
public abstract class AbstractGeneticAlgorithm<S, Result> extends AbstractEvolutionaryAlgorithm<S, Result> { protected int maxPopulationSize ; protected SelectionOperator<List<S>, S> selectionOperator ; protected CrossoverOperator<S> crossoverOperator ; protected MutationOperator<S> mutationOperator ; ... /** * Constructor * @param problem The problem to solve */ public AbstractGeneticAlgorithm(Problem<S> problem) { setProblem(problem); } /** * This method implements a default scheme create the initial population of genetic algorithm * @return */ protected List<S> createInitialPopulation() { List<S> 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<S> selection(List<S> population) { List<S> 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<S> reproduction(List<S> population) { int numberOfParents = crossoverOperator.getNumberOfParents() ; checkNumberOfParents(population, numberOfParents); List<S> offspringPopulation = new ArrayList<>(getMaxPopulationSize()); for (int i = 0; i < getMaxPopulationSize(); i += numberOfParents) { List<S> parents = new ArrayList<>(numberOfParents); for (int j = 0; j < numberOfParents; j++) { parents.add(population.get(i+j)); } List<S> offspring = crossoverOperator.execute(parents); for(S s: offspring){ mutationOperator.execute(s); offspringPopulation.add(s); } } return offspringPopulation; } ... }
run()函数内部就是整个遗传算法的流程了,一目了然。各个具体的遗传算法只需要继承这个类,对很少的函数进行override就得到了一个完整的算法。
比如官方NSGA-II:
public class NSGAII<S extends Solution<?>> extends AbstractGeneticAlgorithm<S, List<S>> { ... /** * Constructor */ public NSGAII(Problem<S> problem, int maxEvaluations, int populationSize, CrossoverOperator<S> crossoverOperator, MutationOperator<S> mutationOperator, SelectionOperator<List<S>, S> selectionOperator, SolutionListEvaluator<S> evaluator) { super(problem); this.maxEvaluations = maxEvaluations; setMaxPopulationSize(populationSize); ; this.crossoverOperator = crossoverOperator; this.mutationOperator = mutationOperator; this.selectionOperator = selectionOperator; this.evaluator = evaluator; } ... @Override protected List<S> replacement(List<S> population, List<S> offspringPopulation) { List<S> jointPopulation = new ArrayList<>(); jointPopulation.addAll(population); jointPopulation.addAll(offspringPopulation); RankingAndCrowdingSelection<S> rankingAndCrowdingSelection ; rankingAndCrowdingSelection = new RankingAndCrowdingSelection<S>(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<T, P extends Problem<?>> implements Solution<T> { private double[] objectives; private List<T> variables; protected P problem ; protected double overallConstraintViolationDegree ; protected int numberOfViolatedConstraints ; protected Map<Object, Object> attributes ; protected final JMetalRandom randomGenerator ; /** * Constructor */ protected AbstractGenericSolution(P problem) { this.problem = problem ; attributes = new HashMap<>() ; randomGenerator = JMetalRandom.getInstance() ; objectives = new double[problem.getNumberOfObjectives()] ; variables = new ArrayList<>(problem.getNumberOfVariables()) ; for (int i = 0; i < problem.getNumberOfVariables(); i++) { variables.add(i, null) ; } } ... }
The Problem:
构造函数中稍微难理解一点的就是这个Problem<S>,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<Double> lowerLimit = new ArrayList<>(getNumberOfVariables()) ; List<Double> 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<DoubleSolution> { private double mutationProbability ; private RandomGenerator<Double> 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<S> getResult() { return getNonDominatedSolutions(getPopulation()); } protected List<S> getNonDominatedSolutions(List<S> solutionList) { return SolutionListUtils.getNondominatedSolutions(solutionList); }
SolutionListUtils类提供了对种群的操作,如对种群按支配关系排序,得到最好最差解等。和4.0版本采用SolutionSet不同的是5.0版本以后采用的是List<Solution>的方式来定义种群。
...............................................................................................................................................................................................................................................................
以上就是整个框架的大致组成了,总结一下:
- 实现AbstractGenericSolution,在具体类里面定义编码方案,定义决策变量,定义优化目标函数储存方法;
- 实现AbstractDoubleProblem,在具体类中定义需要解决的问题,overide evaluate()来定义优化函数;
- 实现各个Operator:选择、交叉、变异;
- 实现AbstractEvolutionaryAlgorithm,Operator提供给具体的Algorithm类,并设定参数;
- 调用run()跑算法,调用getResult()得到结果集。
上述步骤的具体实现在包里几乎都有,所以应用起来会非常简单。
下一节给出NSGA-II的一个具体例子,免得还是云里雾里的。