Pengenalan Perpustakaan Jenetik

1. Pengenalan

Tujuan siri ini adalah untuk menerangkan idea algoritma genetik dan menunjukkan pelaksanaan yang paling terkenal.

Dalam tutorial ini, kami akan menerangkan perpustakaan Java Jenetics yang sangat kuat yang dapat digunakan untuk menyelesaikan pelbagai masalah pengoptimuman.

Sekiranya anda merasa perlu mengetahui lebih lanjut mengenai algoritma genetik, kami mengesyorkan bermula dengan artikel ini.

2. Bagaimana Ia Berfungsi?

Menurut dokumen rasminya, Jenetics adalah perpustakaan berdasarkan algoritma evolusi yang ditulis di Jawa. Algoritma evolusi berakar pada biologi, kerana mereka menggunakan mekanisme yang diilhami oleh evolusi biologi, seperti pembiakan, mutasi, penggabungan, dan pemilihan.

Jenetik dilaksanakan menggunakan antara muka Java Stream , sehingga berfungsi dengan lancar dengan Java Stream API yang lain.

Ciri-ciri utama adalah:

  • pengurangan geseran - tidak perlu mengubah atau mengubah fungsi kecergasan; kita hanya boleh mengubah konfigurasi kelas Engine dan kita sudah bersedia untuk memulakan aplikasi pertama kita
  • bebas kebergantungan - tidak ada perpustakaan pihak ketiga jangka masa yang diperlukan untuk menggunakan Jenetik
  • Java 8 siap - sokongan penuh untuk ekspresi Stream dan lambda
  • multithreaded - langkah evolusi dapat dilaksanakan secara selari

Untuk menggunakan Jenetik, kita perlu menambahkan kebergantungan berikut ke dalam pom.xml kami :

 io.jenetics jenetics 3.7.0 

Versi terbaru boleh didapati di Maven Central.

3. Kes Penggunaan

Untuk menguji semua ciri Jenetik, kami akan berusaha menyelesaikan pelbagai masalah pengoptimuman yang terkenal, bermula dari algoritma binari sederhana dan berakhir dengan masalah Knapsack.

3.1. Algoritma Genetik Mudah

Mari kita anggap bahawa kita perlu menyelesaikan masalah binari termudah, di mana kita perlu mengoptimumkan kedudukan 1 bit dalam kromosom yang terdiri dari 0 dan 1. Pertama, kita perlu menentukan kilang yang sesuai untuk masalah tersebut:

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

Kami membuat BitChromosome dengan panjang 10, dan kebarangkalian memiliki 1 dalam kromosom sama dengan 0,5.

Sekarang, mari buat persekitaran pelaksanaan:

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

Kaedah eval () mengembalikan kiraan bit:

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

Pada langkah terakhir, kita memulakan evolusi dan mengumpulkan hasilnya:

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

Hasil akhirnya akan serupa dengan ini:

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

Kami berjaya mengoptimumkan kedudukan 1 dalam gen.

3.2. Masalah Jumlah Subset

Kes penggunaan lain untuk Jenetik adalah untuk menyelesaikan masalah jumlah subset. Ringkasnya, cabaran untuk mengoptimumkan adalah bahawa, memandangkan satu set bilangan bulat, kita perlu mencari subset yang tidak kosong yang jumlahnya adalah sifar.

Terdapat antara muka yang telah ditentukan di Jenetics untuk menyelesaikan masalah tersebut:

public class SubsetSum implements Problem
    
      { // implementation }
    

Seperti yang dapat kita lihat, kita menerapkan Masalah , yang memiliki tiga parameter:

  • - jenis argumen fungsi kecergasan masalah, dalam kes kami urutan Integer berukuran tetap yang tidak berubah, tersusun ISeq
  • - jenis gen yang digunakan oleh mesin evolusi, dalam kes ini, gen Integer EnumGene yang boleh dikira
  • - jenis hasil fungsi kecergasan; di sini adalah Integer

Untuk menggunakan antara muka Masalah , kita perlu mengganti dua kaedah:

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

Yang pertama, kita menentukan fungsi kecergasan kita, sedangkan yang kedua adalah kelas yang mengandungi kaedah kilang untuk membuat pengekodan masalah biasa, misalnya, untuk mencari subset ukuran tetap terbaik dari satu set asas tertentu, seperti dalam kes kita.

Sekarang kita boleh meneruskan ke bahagian utama. Pada awalnya, kita perlu membuat subset untuk digunakan dalam masalah:

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

Harap maklum bahawa kami menggunakan penjana LCG64ShiftRandom yang disediakan oleh Jenetics. Pada langkah seterusnya, kami membina mesin penyelesaian kami:

Pada langkah seterusnya, kami membina mesin penyelesaian kami:

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

Kami berusaha meminimumkan hasilnya (secara optimum hasilnya adalah 0) dengan menetapkan usia fenotip dan pengubah yang digunakan untuk mengubah keturunan. Pada langkah seterusnya kita dapat memperoleh hasilnya:

Phenotype
    
      result = engine.stream() .limit(limit.bySteadyFitness(55)) .collect(EvolutionResult.toBestPhenotype());
    

Harap diperhatikan bahawa kami menggunakan bySteadyFitness () yang mengembalikan predikat, yang akan memotong aliran evolusi jika tidak ada fenotip yang lebih baik dapat dijumpai setelah jumlah generasi yang diberikan dan mengumpulkan hasil terbaik. Sekiranya kita bernasib baik, dan ada penyelesaian untuk set yang dibuat secara rawak, kita akan melihat sesuatu yang serupa dengan ini:

Sekiranya kita bernasib baik, dan ada penyelesaian untuk set yang dibuat secara rawak, kita akan melihat sesuatu yang serupa dengan ini:

[85|-76|178|-197|91|-106|-70|-243|-41|-98|94|-213|139|238|219] --> 0

Jika tidak, jumlah subset akan berbeza daripada 0.

3.3. Masalah Fit First Knapsack

The Jenetics library allows us to solve even more sophisticated problems, such as the Knapsack problem. Briefly speaking, in this problem, we have a limited space in our knapsack, and we need to decide which items to put inside.

Let's start with defining the bag size and number of items:

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

In the next step, we'll generate a random array containing KnapsackItem objects (defined by size and value fields) and we'll put those items randomly inside the knapsack, using the First Fit method:

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

Next, we need to create the 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();

There are a few points to note here:

  • population size is 500
  • the offspring will be chosen through the tournament and roulette wheel selections
  • as we did in the previous subsection, we need also to define the alterers for the newly created offspring

There is also one very important feature of Jenetics. We can easily collect all statistics and insights from the whole simulation duration. We are going to do this by using the EvolutionStatistics class:

EvolutionStatistics statistics = EvolutionStatistics.ofNumber();

Finally, let's run the simulations:

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

Please note that we are updating the evaluation statistics after each generation, which is limited to 7 steady generation and a maximum of 100 generations in total. In more detail there are two possible scenarios:

  • we achieve 7 steady generations, then the simulation stops
  • we cannot get 7 steady generations in less than 100 generations, so the simulation stops due to the second limit()

It's important to have maximum generations limit, otherwise, the simulations may not stop in a reasonable time.

The final result contains a lot of information:

+---------------------------------------------------------------------------+ | 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 | +---------------------------------------------------------------------------+

This particular time, we were able to put items with a total value of 716,68 in the best scenario. We also can see the detailed statistics of evolution and time.

How to test?

It is a fairly simple process — just open the main file related to the problem and first run the algorithm. Once we have a general idea, then we can start playing with the parameters.

4. Conclusion

In this article, we covered the Jenetics library features based on real optimization problems.

The code is available as a Maven project on GitHub. Please note that we provided the code examples for more optimization challenges, such as the Springsteen Record (yes, it exists!) and Traveling Salesman problems.

Untuk semua artikel dalam siri ini, termasuk contoh algoritma genetik lain, periksa pautan berikut:

  • Cara Merangka Algoritma Genetik di Jawa
  • Masalah Penjual Perjalanan di Jawa
  • Pengoptimuman Koloni Semut
  • Pengenalan kepada perpustakaan Jenetik (ini)