Xavier Nayrac

Rubyiste accro au TDD, serial blogger, apprenti data scientist, heureux utilisateur de Vim, accordéoniste.
Si vous vous sentez particulièrement généreux, suivez moi sur Twitter.

Les algorithmes génétiques démystifiés 5

| Comments

Niveau : intermédiaire

Maintenant qu’on sait créer une population de solutions, l’évaluer, opérer une sélection des meilleures solutions et obtenir une nouvelle génération par la reproduction, il reste à assembler toutes ces parties et voir ce qu’il se passe…

Voici le code complet de notre algorithme:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def make_chromosome
  value = ""
  48.times { value += rand(0..1).to_s }
  [nil, value]
end

def make_population
  population = []
  100.times { population << make_chromosome }
  population
end

def chromosome_to_gene(chromosome)
  chromosome.last.scan /.{4}/
end

def gene_to_operand(gene)
  case gene
  when "0000" then 0
  when "0001" then 1
  when "0010" then 2
  when "0011" then 3
  when "0100" then 4
  when "0101" then 5
  when "0110" then 6
  when "0111" then 7
  when "1000" then 8
  when "1001" then 9
  when "1010" then "+"
  when "1011" then "-"
  when "1100" then "/"
  when "1101" then "%"
  end
end

def selection
  @selected = @population[0...50].shuffle
end

def genes_to_formula(genes)
  formula = []
  genes.each {|gene| formula << gene_to_operand(gene) }
  formula.join
end

def evaluation(genes)
  formula = genes_to_formula(genes)
  begin
    result = (@search_value - eval(formula)).abs
  rescue Exception
    result = 999_999_999_999
  end
  result = 999_999_999_999 unless result.is_a?(Integer)
  result
end

def score_population
  @population = @population.map do |individual|
    genes = chromosome_to_gene(individual)
    individual[0] = evaluation(genes)
    individual
  end
end

def crossover(parent1, parent2)
  point = rand(1..47)
  child1 = [nil, parent1.last[0...point] + parent2.last[point..-1]]
  child2 = [nil, parent2.last[0...point] + parent1.last[point..-1]]
  [child1, child2]
end

def next_generation
  @selection = selection
  @population = []
  25.times do
    parent1, parent2 = @selection.slice!(0, 2)
    child1, child2 = crossover(parent1, parent2)
    child3, child4 = crossover(parent1, parent2)
    @population += [child1, child2, child3, child4]
  end
end

# Ce qui suit est nouveau:

@search_value = 987
@population = make_population

50.times do |generation|
  score_population
  @population = @population.sort
  best = @population.first.first
  puts "Generation: #{generation} Best: #{best}"
  exit if best == 0
  next_generation
end

Seules les dernières lignes de code sont nouvelles. Elles sont assez simple à comprendre je pense, même si vous ne connaissez pas le langage Ruby. On commence par définir le nombre qu’on recherche puis on crée la population d’origine au hasard avec @population = make_population. Ensuite on itère sur 50 générations avec 50.times do |generation|. C’est notre première condition de sortie : quoiqu’il se passe, on arrête le traitement au bout à la 50ème génération. Dans cette boucle on évalue la génération en cours avec score_population et on la trie. Pour savoir où on en est visuellement on extrait le meilleur score avec best = @population.first.first et on affiche cette information à la ligne suivante. Vient ensuite notre seconde et dernière condition de sortie avec exit if best == 0 ; autrement dit on stoppe le traitement à la première solution trouvée. Pour finir, on produit la génération suivante avec next_generation.

Et ça donne quoi ?

La plupart du temps ça donne quelque chose comme ça:

1
2
3
4
5
6
7
8
9
10
11
12
[~/genetic]⇒ ruby test.rb 
Generation: 0 Best: 507
Generation: 1 Best: 138
Generation: 2 Best: 485
Generation: 3 Best: 347
Generation: 4 Best: 65
Generation: 5 Best: 208
Generation: 6 Best: 222
Generation: 7 Best: 15
Generation: 8 Best: 15
Generation: 9 Best: 2
Generation: 10 Best: 0

Les générations successives convergent lentement vers la solution. Si vous voulez voir la solution trouvée (c’est normal d’être curieux) vous pouvez remplacez une ligne de code pour afficher la solution:

1
2
3
4
5
6
# exit if best == 0
if best == 0
  genes = chromosome_to_gene(@population.first)
  puts "Formula: #{genes_to_formula(genes)}"
  exit
end

Voici quelques exemples de solutions:

Formula: 912%429+933
Formula: 670+594-277
Formula: 893+91%96+3
Formula: 923--03--61

Alors, la plupart du temps, ça se passe bien. Mais parfois un phénomène étrange se produit:

1
2
3
4
5
6
7
8
9
10
[~/genetic]⇒ ruby test.rb 
Generation: 0 Best: 597
Generation: 1 Best: 621
Generation: 2 Best: 104
...
Generation: 25 Best: 1
...
Generation: 48 Best: 1
Generation: 49 Best: 1
[~/genetic]⇒ 

L’algorithme reste bloqué sur une valeur et on atteint la 50ème génération sans avoir trouver de solution. What the fuck ? Et bien l’algorithme a atteint ce qu’on appelle l’extremum local. Pour faire court, ça signifie qu’il ne peut pas faire mieux avec les gènes dont il disposait à l’origine. Je developperais cette idée dans un futur article. En attendant, comment on règle ça ? En s’inspirant encore une fois de phénomènes naturels : la mutation et/ou la diversité génétique. C’est le sujet du prochain article.

À demain.

Articles connexes

Commentaires