Dans l’article précédent j’ai donné l’objectif de ce second algorithme, inspiré par le paradoxe du singe savant, et on a vu comment construire la population de phrases. Aujourd’hui, on se concentre sur la méthode d’évaluation, tout en introduisant l’idée de «normaliser un score».

Une fois la population créée, il faut l’évaluer avant de pouvoir passer à l’étape de sélection. Je rappelle que nos chromosomes (que j’appelle aussi individus ou encore solutions potentielles) sont mémorisés dans une liste contenant un score et une phrase (voir Les algorithmes génétiques démystifiés 8).

Voici comment j’évalue la population:

def evaluate_population
  @population.map! {|person| [evaluate(person.last), person.last] }
end

def evaluate(phrase)
  score = 0
  phrase.split('').each_with_index do |character, index|
    score += 1 if @search_value[index] == character
  end
  score
end

Je donne 1 point pour chaque lettre bien placée. C’est difficile de faire plus simple. Je pense que dans un autre article, on expérimentera une autre manière d’évaluer les phrases en attribuant aussi des points pour les lettres mal placées.

Contrairement à l’algorithme précédent, je vais aller plus loin en transformant ces scores en pourcentages. Utiliser les pourcentages sera très utile pour la méthode de sélection que je développerais dans le prochain article. Pour transformer les scores en pourcentages, on doit d’abord les normaliser. Cela signifie qu’on va transformer chaque score en un nombre compris entre 0 et 1. Pour normaliser, on calcule la somme de tous les scores de la population et on divise chaque score par ce total. Il suffit ensuite de multiplier par 100 pour avoir un pourcentage:

def normalize_population_score
  total = @population.inject(0) {|sum, person| sum + person.first }
  @population.map! {|person| [person.first.to_f / total * 100, person.last] }
end

Finalement, on met tout ça ensemble dans une méthode:

def score_population
  evaluate_population
  normalize_population_score
end

Voici notre programme monkey.rb pour l’instant:

def make_chromosome
  value = ""
  length = @search_value.size
  length.times { value += random_gene }
  [nil, value]
end

def random_gene
  @genes[rand(@genes.size)]
end

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

def score_population
  evaluate_population
  normalize_population_score
end

def evaluate_population
  @population.map! {|person| [evaluate(person.last), person.last] }
end

def evaluate(phrase)
  score = 0
  phrase.split('').each_with_index do |character, index|
    score += 1 if @search_value[index] == character
  end
  score
end

def normalize_population_score
  total = @population.inject(0) {|sum, person| sum + person.first }
  @population.map! {|person| [person.first.to_f / total * 100, person.last] }
end

@search_value = "Mon royaume pour un cheval"
@genes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ "
@population_size = 100
@population = make_population
score_population
@population.each {|i| puts i.inspect }

Et voici ce que ça donne:

[~/genetic]⇒ ruby monkey.rb 
[2.083333333333333, "GPjvZUOnEHAwBuVPazOXXYhwaG"]
[0.0, "yQdkKetHFCUpMSMjVFwepXREhT"]
[4.166666666666666, "Fz pHfkVjyRoIhgGglvfWhXhpl"]
[0.0, "fwjafrGAalfDRhpnpAtUoNfVNU"]
[0.0, "zXxPlALVVKxGg sOUdKpSAdKNG"]
[0.0, "myoLBtIbKhfNQPnHUzqHkw Mjz"]
[0.0, "HXDVgzNAKoUhjbVPLLNikGdWqX"]
[0.0, "XQIqLRKNzrxXJUqWRFQpYozNMB"]
[0.0, "xuIIUHEwaAdFcVedVJXkTJjFEv"]
.
.
.
[6.25, " oFkamnafTYpazNMRPY KCEVLZ"]
[2.083333333333333, "JqxvMEowRmEzeRPUwXJdCQQ UB"]

La prochaine fois, on parlera de la méthode de sélection dite de «la roue de la fortune», secondée par une piscine d’accouplement (oui, c’est bien le terme, véridique !).

À demain.