1. 简介
本系列的目的是解释遗传算法的思想并展示最为人所知的实现。
在本教程中,我们将介绍一个非常强大的Jenetics Java库,它可用于解决各种优化问题。
如果你觉得需要了解更多有关遗传算法的知识,我们建议你从这篇文章开始。
2. 它是如何工作的?
根据其官方文档,Jenetics是一个基于Java编写的进化算法库。进化算法源于生物学,因为它们使用受生物进化启发的机制,例如繁殖、突变、重组和选择。
Jenetics是使用Java Stream接口实现的,因此它可以与Java Stream API的其余部分顺利协作。
主要特点有:
- 无摩擦最小化-无需改变或调整适应度函数;我们只需更改Engine类的配置,即可启动我们的第一个应用程序
- 无依赖性-使用Jenetics不需要运行时第三方库
- Java 8就绪-全面支持Stream和Lambda表达式
- 多线程-进化步骤可以并行执行
为了使用Jenetics,我们需要在pom.xml中添加以下依赖:
<dependency>
<groupId>io.jenetics</groupId>
<artifactId>jenetics</artifactId>
<version>3.7.0</version>
</dependency>
最新版本可以在Maven Central找到。
3. 用例
为了测试Jenetics的所有功能,我们将尝试解决各种众所周知的优化问题,从简单的二进制算法开始到背包问题结束。
3.1 简单遗传算法
假设我们需要解决一个最简单的二进制问题,即优化由0和1组成的染色体中1位的位置。首先,我们需要定义适合该问题的工厂:
Factory<Genotype<BitGene>> gtf = Genotype.of(BitChromosome.of(10, 0.5));
我们创建了长度为10的BitChromosome,染色体中出现1的概率等于0.5。
现在,让我们创建执行环境:
Engine<BitGene, Integer> engine = Engine.builder(SimpleGeneticAlgorithm::eval, gtf).build();
eval()方法返回位数:
private Integer eval(Genotype<BitGene> gt) {
return gt.getChromosome().as(BitChromosome.class).bitCount();
}
在最后一步,我们开始进化并收集结果:
Genotype<BitGene> result = engine.stream()
.limit(500)
.collect(EvolutionResult.toBestGenotype());
最终结果将类似于此:
Before the evolution:
[00000010|11111100]
After the evolution:
[00000000|11111111]
我们设法优化了基因中1的位置。
3.2 子集和问题
Jenetics的另一个用例是解决子集和问题;简而言之,优化的挑战在于,给定一组整数,我们需要找到一个和为0的非空子集。
Jenetics中预定义了一些接口来解决此类问题:
public class SubsetSum implements Problem<ISeq<Integer>, EnumGene<Integer>, Integer> {
// implementation
}
如我们所见,我们实现了Problem<T, G, C>,它有3个参数:
- <T>:问题适应度函数的参数类型,在我们的例子中是一个不可变、有序、固定大小的整数序列ISeq<Integer>
- <G>:进化引擎正在处理的基因类型,在本例中为可数Integer基因EnumGene<Integer>
- <C>:适应度函数的结果类型;这里是Integer
为了使用Problem<T, G, C>接口,我们需要重写两个方法:
@Override
public Function<ISeq<Integer>, Integer> fitness() {
return subset -> Math.abs(subset.stream()
.mapToInt(Integer::intValue).sum());
}
@Override
public Codec<ISeq<Integer>, EnumGene<Integer>> codec() {
return codecs.ofSubSet(basicSet, size);
}
在第一个中,我们定义了适应度函数,而第二个是一个包含用于创建常见问题编码的工厂方法的类,例如,从给定的基本集中找到最佳的固定大小子集,就像我们的情况一样。
现在我们可以进入正题了,首先,我们需要创建一个子集来解决这个问题:
SubsetSum problem = of(500, 15, new LCG64ShiftRandom(101010));
请注意,我们使用了Jenetics提供的LCG64ShiftRandom生成器。
下一步,我们将构建解决方案的引擎:
Engine<EnumGene<Integer>, Integer> engine = Engine.builder(problem)
.minimizing()
.maximalPhenotypeAge(5)
.alterers(new PartiallyMatchedCrossover<>(0.4), new Mutator<>(0.3))
.build();
我们尝试通过设置表型年龄和用于改变后代的改变因此来最小化结果(最佳结果为0),在下一步中,我们可以得到结果:
Phenotype<EnumGene<Integer>, Integer> result = engine.stream()
.limit(limit.bySteadyFitness(55))
.collect(EvolutionResult.toBestPhenotype());
请注意,我们使用了bySteadyFitness()来返回谓词,如果在给定的代数之后找不到更好的表型,它将截断进化流并收集最佳结果。
如果我们很幸运,并且随机创建的集合有一个解,我们将看到类似这样的内容:
[85|-76|178|-197|91|-106|-70|-243|-41|-98|94|-213|139|238|219] --> 0
否则,子集的总和将不为0。
3.3 背包首次适合问题
Jenetics库使我们能够解决更复杂的问题,例如背包问题。简而言之,在这个问题中,我们的背包空间有限,我们需要决定将哪些物品放入其中。
让我们首先定义袋子的大小和物品数量:
int nItems = 15;
double ksSize = nItems * 100.0 / 3.0;
在下一步中,我们将生成一个包含KnapsackItem对象(由size和value字段定义)的随机数组,并使用First Fit方法将这些物品随机放入背包中:
KnapsackFF ff = new KnapsackFF(Stream.generate(KnapsackItem::random)
.limit(nItems)
.toArray(KnapsackItem[]::new), ksSize);
接下来,我们需要创建Engine:
Engine<BitGene, Double> engine = Engine.builder(ff, BitChromosome.of(nItems, 0.5))
.populationSize(500)
.survivorsSelector(new TournamentSelector<>(5))
.offspringSelector(new RouletteWheelSelector<>())
.alterers(new Mutator<>(0.115), new SinglePointCrossover<>(0.16))
.build();
这里有几点需要注意:
- 种群规模为500
- 后代将通过锦标赛和轮盘赌来选择
- 正如我们在上一小节中所做的那样,我们还需要为新创建的后代定义修改器
Jenetics还有一个非常重要的功能,我们可以轻松收集整个模拟过程中的所有统计数据和见解,我们将使用EvolutionStatistics类来实现这一点:
EvolutionStatistics<Double, ?> statistics = EvolutionStatistics.ofNumber();
最后,让我们运行模拟:
Phenotype<BitGene, Double> best = engine.stream()
.limit(bySteadyFitness(7))
.limit(100)
.peek(statistics)
.collect(toBestPhenotype());
请注意,我们会在每一代之后更新评估统计数据,这限制为7个稳定代,总共最多100代。更详细地说,有两种可能的情况:
- 我们实现了7代稳定演化,然后模拟停止
- 我们无法在少于100代的时间内获得7个稳定的代,因此模拟由于第二个limit()而停止
拥有最大代数限制非常重要,否则模拟可能不会在合理的时间内停止。
最终结果包含了很多信息:
+---------------------------------------------------------------------------+
| Time statistics |
+---------------------------------------------------------------------------+
| Selection: sum=0,039207931000 s; mean=0,003267327583 s |
| Altering: sum=0,065145069000 s; mean=0,005428755750 s |
| Fitness calculation: sum=0,029678433000 s; mean=0,002473202750 s |
| Overall execution: sum=0,111383965000 s; mean=0,009281997083 s |
+---------------------------------------------------------------------------+
| Evolution statistics |
+---------------------------------------------------------------------------+
| Generations: 12 |
| Altered: sum=7 664; mean=638,666666667 |
| Killed: sum=0; mean=0,000000000 |
| Invalids: sum=0; mean=0,000000000 |
+---------------------------------------------------------------------------+
| Population statistics |
+---------------------------------------------------------------------------+
| Age: max=10; mean=1,792167; var=4,657748 |
| Fitness: |
| min = 0,000000000000 |
| max = 716,684883338605 |
| mean = 587,012666759785 |
| var = 17309,892287851708 |
| std = 131,567063841418 |
+---------------------------------------------------------------------------+
这次,我们能够在最佳情况下放置总价值为71668的物品,我们还可以看到进化和时间的详细统计数据。
如何测试?
这是一个相当简单的过程-只需打开与问题相关的主文件,然后先运行算法即可。一旦我们有了大致的想法,就可以开始调整参数了。
4. 总结
在本文中,我们根据实际优化问题介绍了Jenetics库的功能。
有关该系列的所有文章,包括遗传算法的其他示例,请查看以下链接:
- 如何用Java设计遗传算法
- Java中的旅行商问题
- 蚁群优化
- Jenetics库简介(本文)
Post Directory
