一、前言
最近,我发现一个开源库,基于该库能够较为简单地使用Java实现NSGA-II算法。
NSGA-II相关内容可以参考我之前的博客。
开源库github:https://github.com/onclave/NSGA-II
注:对于Java实现NSGA-II,使用较为广泛的库是JMetal,个人感觉相对来说上手更难,感兴趣的人可以看看,此博客不予说明。
二、使用
2.1、简单上手
以下代码是文档中给出的默认实现方式,不进行任何其他操作,仅使用如下代码运行的话,会使用默认配置。
public static void main(String[] args) {
NSGA2 nsga2 = new NSGA2();
nsga2.run();
}
根据源代码,可知默认配置如下:
- 种群大小:100
- 进化次数:25
- 染色体长度:20
- 目标函数:SCH基准函数
- 编码方式:二进制编码
- 交叉方式:均匀交叉(基于拥挤度比较的二进制锦标赛选择参与交叉的双亲)
- 变异方式:单点变异
注:SCH基准函数来源于论文《Multiple objective optimization with vector evaluated genetic algorithms》,在提出NSGA-II的论文中也有部分描述。
在成功运行之后,除了会在运行目录下生成日志文件以外,还会弹出两个图像界面,一个是最终的Pareto解集,另一个是每一代的Pareto解集。
除了提供SCH目标函数以外,还提供了ZDT1基准函数。
2.2、定制实现
如果需要自定义实现,可以按照以下顺序进行。
2.2.1、定制等位基因
在遗传算法中,种群Population
由多个染色体(或称个体)Chromosomes
组成,而染色体由多个等位基因Alleles
组成。
通常来讲,定义等位基因可以认为就是定义编码方式。
因此,定制NSGA-II算法的第一步首先是定义等位基因,此库已经实现了BooleanAllele
、IntegerAllele
、ValueAllele
等位基因,对应于二进制编码、整数编码、浮点数(实数)编码。
自定义等位基因也比较简单,只需继承包里所提供的抽象类AbstractAllele
并实现两个抽象方法即可。
例如:
显然,getGene
表示获取基因值,getCopy
表示获取一个相同值的新基因。
public class MyAllele extends AbstractAllele {
public MyAllele(double gene) {
super(gene);
}
@Override
public Double getGene() {
return (Double)this.gene;
}
@Override
public AbstractAllele getCopy() {
return new MyAllele((Double)this.gene);
}
}
2.2.2、定制遗传密码生成器
遗传密码生成器被用来生成一条染色体,自定义生成器需要实现GeneticCodeProducer
接口。
例如:
public class GeneticCode implements GeneticCodeProducer {
//随机创建一条长度为length的二进制编码染色体
@Override
public List<BooleanAllele> produce(int length) {
List<BooleanAllele> geneticCode = new ArrayList();
for (int i = 0; i < length; ++i) {
geneticCode.add(i, new BooleanAllele(ThreadLocalRandom.current().nextBoolean()));
}
return geneticCode;
}
}
官方提供了binaryGeneticCodeProducer
、valueEncodedGeneticCodeProducer
、permutationEncodingGeneticCodeProducer
生成器。
2.2.3、定制初始种群和子代种群生成方式
定制初始种群生成需要实现PopulationProducer
接口。
例如:
public class Initpop implements PopulationProducer {
@Override
public Population produce(int populationSize, int chromosomeLength, GeneticCodeProducer geneticCodeProducer, FitnessCalculator fitnessCalculator) {
List<Chromosome> populace = new ArrayList();
for(int i = 0; i < populationSize; ++i) {
populace.add(new Chromosome(geneticCodeProducer.produce(chromosomeLength)));
}
return new Population(populace);
}
}
定制子种群生成需要实现ChildPopulationProducer
接口。
例如:
public class Childpop implements ChildPopulationProducer {
@Override
public Population produce(Population parentPopulation, AbstractCrossover crossover, AbstractMutation mutation, int populationSize) {
List<Chromosome> populace = new ArrayList();
while (true) {
while (populace.size() < populationSize) {
if (populationSize - populace.size() == 1) {
populace.add(mutation.perform(Service.crowdedBinaryTournamentSelection(parentPopulation)));
} else {
Iterator var5 = crossover.perform(parentPopulation).iterator();
while (var5.hasNext()) {
Chromosome chromosome = (Chromosome) var5.next();
populace.add(mutation.perform(chromosome));
}
}
}
return new Population(populace);
}
}
}
2.2.4、定制目标函数
仅需继承抽象类AbstractObjectiveFunction
,实现getValue
方法即可。
可以为目标函数设置标题,仅需给objectiveFunctionTitle
赋值,所设置的标题会显示在迭代结束后的图像中。
例子:
public class ZDT1_1 extends AbstractObjectiveFunction {
public ZDT1_1() {
this.objectiveFunctionTitle = "x1";
}
@Override
public double getValue(Chromosome chromosome) {
return chromosome.getGeneticCode().stream().map(e -> (ValueAllele) e).collect(Collectors.toList()).get(0).getGene();
}
}
实现目标函数,可以选择实现FitnessCalculator,它可以计算染色体的一些适应度。NSGA-II没有任何直接的适应度计算,默认情况下这是不需要的。但这可以作为用户可能希望根据需要添加到算法中的一些额外参数计算,或者如果目标函数需要任何额外的计算参数,则可以作为目标函数的参数,可以参考默认配置的SCH目标函数。
注意,此包默认为目标函数越大越好,因此,如果定制的目标函数需要满足越小越好,可以使用取反的方式。
以下是作者原话:
Prepare your own objective functions against your dataset. They can be maximization problems or minimization problems. This library considers all objective functions to be maximization problems. Hence, for any minimization problem, take its inverse.
根据数据集准备自己的目标函数。它们可以是最大化问题或最小化问题。该库认为所有目标函数都是最大化问题。因此,对于任何最小化问题,取其逆。
2.2.5、定制交叉算子
需要做两件事:
- 编写类实现
CrossoverParticipantCreator
接口,实现的方法用来选择参与交叉的双亲染色体。 - 编写类继承
AbstractCrossover
类,该类用来实现交叉算子。
例如:
//实现交叉双亲选择
//此示例为随机从种群中选择两个染色体
public class MyParticipantCreator implements CrossoverParticipantCreator {
@Override
public List<Chromosome> create(Population p) {
List<Chromosome> selected = new ArrayList();
selected.add(p.getPopulace().get(ThreadLocalRandom.current().nextInt(0, p.size())));
selected.add(p.getPopulace().get(ThreadLocalRandom.current().nextInt(0, p.size())));
return selected;
}
}
//实现交叉算子
public class Mycross extends AbstractCrossover {
public Mycross(CrossoverParticipantCreator crossoverParticipantCreator) {
super(crossoverParticipantCreator);
}
@Override
public List<Chromosome> perform(Population population) {
....
}
}
官方默认提供了selectByBinaryTournamentSelection
,使用基于拥挤度比较的二进制锦标赛选择来选择参与交叉的双亲。
而对于交叉算子,官方也提供了部分实现:均匀交叉UniformCrossover
、顺序交叉OrderCrossover
、模拟二进制交叉SimulatedBinaryCrossover
。
2.2.6、定制变异算子
相较于定制交叉算子更简单,只需要编写类继承抽象类AbstractMutation
。
例如:
public class Mymutation extends AbstractMutation {
@Override
public Chromosome perform(Chromosome chromosome) {
...
}
}
与交叉算子相同,官方也提供了一些实现:多项式变异PolynomialMutation
、单点变异SinglePointMutation
、交换变异SwapMutation
。
已实现的多项式变异仅适用于浮点数(实数)编码,单点变异仅适用于二进制编码。
2.3、配置并运行
实现完各种定制之后,创建一个Configuration
对象并将配置中的实例换成定制的即可,
最后将Configuration
传递给NSGA对象。
public static void main(String[] args) {
//创建配置类
Configuration configuration = new Configuration();
configuration.setGenerations(250);
configuration.setChromosomeLength(30);
...
//创建NSGA2类
NSGA2 nsga2 = new NSGA2(configuration);
nsga2.run();
}
三、总结
以上就是我个人学习并使用该库的一些总结与心得。
建议在使用之前,最好先了解遗传算法以及NSGA-II算法。
除了可以自定义常规的遗传算子以外,还支持自定义迭代终止条件,需要实现TerminatingCriterion
接口,有需要的读者可以尝试实现,库中还提供了一个工具类Service
,包含了一些数据处理的方法,能够简化操作。