Введение в библиотеку Jenetics

Введение в библиотеку Jenetics

1. Вступление

The aim of this series должен объяснить идею генетических алгоритмов и показать наиболее известные реализации.

В этом руководстве мы будемdescribe a very powerful Jenetics Java library that can be used for solving various optimization problems.

Если вы чувствуете, что вам нужно больше узнать о генетических алгоритмах, мы рекомендуем начать сthis article.

2. Как это работает?

Согласноofficial documents, Jenetics - это библиотека, основанная на эволюционном алгоритме, написанном на Java. Эволюционные алгоритмы имеют свои корни в биологии, поскольку они используют механизмы, вдохновленные биологической эволюцией, такие как размножение, мутация, рекомбинация и отбор.

Jenetics реализован с использованием интерфейса JavaStream, поэтому он без проблем работает с остальной частью API JavaStream.

Основными функциями являются:

  • frictionless minimization - изменять или настраивать фитнес-функцию не нужно; мы можем просто изменить конфигурацию классаEngine, и мы готовы запустить наше первое приложение

  • dependency free - для использования Jenetics не требуются сторонние библиотеки времени выполнения.

  • Java 8 ready - полная поддержкаStream и лямбда-выражений

  • multithreaded - эволюционные шаги могут выполняться параллельно

Чтобы использовать Jenetics, нам нужно добавить следующую зависимость в нашpom.xml:


    io.jenetics
    jenetics
    3.7.0

Последнюю версию можно найтиin Maven Central.

3. Случаи применения

Чтобы протестировать все возможности Jenetics, мы попробуем решить различные известные задачи оптимизации, начиная с простого бинарного алгоритма и заканчивая задачей о ранце.

3.1. Простой генетический алгоритм

Предположим, нам нужно решить простейшую двоичную задачу, в которой нам нужно оптимизировать положение битов 1 в хромосоме, состоящей из нулей и единиц. Сначала нам нужно определить фабрику, подходящую для задачи:

Factory> gtf = Genotype.of(BitChromosome.of(10, 0.5));

Мы создалиBitChromosome длиной 10 и вероятностью появления единиц в хромосоме, равной 0,5.

Теперь давайте создадим среду выполнения:

Engine engine
  = Engine.builder(SimpleGeneticAlgorithm::eval, gtf).build();

Методeval() возвращает количество битов:

private Integer eval(Genotype gt) {
    return gt.getChromosome().as(BitChromosome.class).bitCount();
}

На последнем этапе мы начинаем эволюцию и собираем результаты:

Genotype result = engine.stream()
  .limit(500)
  .collect(EvolutionResult.toBestGenotype());

Окончательный результат будет выглядеть примерно так:

Before the evolution:
[00000010|11111100]
After the evolution:
[00000000|11111111]

Нам удалось оптимизировать положение единиц в гене.

3.2. Задача суммы подмножества

Другой вариант использования Jenetics - решениеsubset sum problem. Вкратце, задача оптимизации состоит в том, что, учитывая набор целых чисел, нам нужно найти непустое подмножество, сумма которого равна нулю.

В Jenetics есть предопределенные интерфейсы для решения таких проблем:

public class SubsetSum implements Problem, EnumGene, Integer> {
    // implementation
}

Как мы видим, мы реализуемProblem<T, G, C>, который имеет три параметра:

  • <T> - тип аргумента функции пригодности задачи, в нашем случае неизменяемая, упорядоченная последовательностьInteger фиксированного размераISeq<Integer>

  • <G> - тип гена, с которым работает двигатель эволюции, в данном случае счетныеInteger геныEnumGene<Integer>

  • <C> - тип результата фитнес-функции; вот этоInteger

Чтобы использовать интерфейсProblem<T, G, C>, нам нужно переопределить два метода:

@Override
public Function, Integer> fitness() {
    return subset -> Math.abs(subset.stream()
      .mapToInt(Integer::intValue).sum());
}

@Override
public Codec, EnumGene> codec() {
    return codecs.ofSubSet(basicSet, size);
}

В первом из них мы определяем нашу фитнес-функцию, а во втором - класс, содержащий фабричные методы для создания общих кодовых задач, например, для поиска лучшего подмножества фиксированного размера из данного базового набора, как в нашем случае.

Теперь мы можем перейти к основной части. Сначала нам нужно создать подмножество для использования в задаче:

SubsetSum problem = of(500, 15, new LCG64ShiftRandom(101010));

Обратите внимание, что мы используем генераторLCG64ShiftRandom, предоставленный Jenetics. На следующем этапе мы создаем двигатель нашего решения:

На следующем этапе мы создаем двигатель нашего решения:

Engine, Integer> engine = Engine.builder(problem)
  .minimizing()
  .maximalPhenotypeAge(5)
  .alterers(new PartiallyMatchedCrossover<>(0.4), new Mutator<>(0.3))
  .build();

Мы стараемся минимизировать результат (оптимально результат будет 0), устанавливая возраст фенотипа и альтеры, используемые для изменения потомства. На следующем шаге мы можем получить результат:

Phenotype, 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 позволяет нам решать даже более сложные задачи, такие какKnapsack problem. Короче говоря, в этой задаче у нас есть ограниченное пространство в нашем рюкзаке, и мы должны решить, какие предметы положить внутрь.

Начнем с определения размера сумки и количества предметов:

int nItems = 15;
double ksSize = nItems * 100.0 / 3.0;

На следующем этапе мы сгенерируем случайный массив, содержащий объектыKnapsackItem (определяемые полямиsize иvalue), и поместим эти элементы случайным образом в рюкзак, используя первый Метод подгонки:

KnapsackFF ff = new KnapsackFF(Stream.generate(KnapsackItem::random)
  .limit(nItems)
  .toArray(KnapsackItem[]::new), ksSize);

Далее нам нужно создатьEngine:

Engine 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

  • потомство будет выбрано через турнир и выбор колеса рулетки

  • как мы это делали в предыдущем подразделе, нам нужно также определить альтеры для вновь созданного потомства

There is also one very important feature of Jenetics. We can easily collect all statistics and insights from the whole simulation duration. Мы собираемся сделать это с помощью классаEvolutionStatistics:

EvolutionStatistics statistics = EvolutionStatistics.ofNumber();

Наконец, давайте запустим моделирование:

Phenotype best = engine.stream()
  .limit(bySteadyFitness(7))
  .limit(100)
  .peek(statistics)
  .collect(toBestPhenotype());

Обратите внимание, что мы обновляем статистику оценки после каждого поколения, которая ограничена 7 устойчивыми поколениями и в общей сложности максимум 100 поколениями. Более подробно есть два возможных сценария:

  • мы достигаем 7 устойчивых поколений, затем моделирование останавливается

  • мы не можем получить 7 устойчивых поколений менее чем за 100 поколений, поэтому симуляция останавливается из-за второго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                              |
+---------------------------------------------------------------------------+

Именно в это время мы смогли поместить предметы с общей стоимостью 716,68 в лучший сценарий. Мы также можем увидеть подробную статистику эволюции и времени.

Как проверить?

Это довольно простой процесс - просто откройте основной файл, связанный с проблемой, и сначала запустите алгоритм. Как только у нас появится общая идея, мы можем начать играть с параметрами.

4. Заключение

В этой статье мы рассмотрели возможности библиотеки Jenetics, основанные на реальных проблемах оптимизации.

Код доступен как проект Maven наGitHub. Обратите внимание, что мы предоставили примеры кода для решения других задач оптимизации, таких какSpringsteen Record (да, он существует!) И задачи коммивояжера.

Для всех статей в серии, включая другие примеры генетических алгоритмов, проверьте следующие ссылки: