597 lines
19 KiB
Markdown
597 lines
19 KiB
Markdown
## Les algorithmes génétiques
|
||
|
||
* Présentation
|
||
|
||
- Les *algorithmes génétiques* font partie des algorithmes
|
||
évolutionnistes.
|
||
- S'inspirent des mécanismes de sélection naturelle pour approcher
|
||
une solution d'un problème *d'optimisation*.
|
||
- S'appliquent bien dans le cas où l'espace des solutions est grand.
|
||
|
||
|
||
* Darwin
|
||
|
||
- /De l'origine des espèces/
|
||
- les individus les mieux adaptés à leur environnement survivent et
|
||
se reproduisent davantage en transmettant les variations utiles à
|
||
la survie.
|
||
- L'espèce évolue en s'adaptant à son environnement.
|
||
- C'est le principe de *sélection naturelle*
|
||
|
||
|
||
* Sélection naturelle
|
||
|
||
Trois principes :
|
||
- Variation : les individus diffèrent les uns des autres et d'une
|
||
génération à l'autre ;
|
||
- Adaptation : les individus les mieux adaptés survivent et se
|
||
reproduisent davantage ;
|
||
- Hérédité : les caractéristiques sont héréditaires et se
|
||
transmettent de génération en génération.
|
||
|
||
|
||
* Principes
|
||
|
||
** Problèmes et solution
|
||
|
||
On dispose d'un problème d'optimisation :
|
||
|
||
$$\max_{s\in R}\lbrace f(s) \rbrace$$
|
||
|
||
- $R$ est l'espace des solutions *réalisables*
|
||
- $f$ est la fonction *objectif*, où encore :
|
||
- fonction d'évaluation ,
|
||
- fonction d'adaptation ,
|
||
- fonction de fitness.
|
||
|
||
|
||
|
||
** Le principe des algorithmes génétiques
|
||
|
||
- travailler avec un sous-ensemble $P$ de $R$ appelé *population* ;
|
||
- On fait évoluer $P$ en lui appliquent des opérateurs génétiques
|
||
pour produire une nouvelle génération ;
|
||
- On favorise les individus les mieux adaptés (valeur de $f$ plus
|
||
élevée) lors de la production de la génération suivante ;
|
||
- Après $n$ générations, on choisit un individu dans la population.
|
||
|
||
|
||
** génome
|
||
|
||
- Les individus sont représentés par leur génôme : suite finie de
|
||
$k$ symboles ;
|
||
- Ces symboles représentent les caractéristiques d'un individu,
|
||
c'est-à-dire un gène ;
|
||
|
||
Chercher la solution optimale consiste à chercher une séquence de
|
||
symboles optimale.
|
||
|
||
Idée à respecter : plus l'adaptation est élevée, plus l'individu
|
||
est proche de la solution.
|
||
|
||
** Opérateurs génétiques
|
||
|
||
Il existe trois opérateurs génétiques :
|
||
|
||
*** la *sélection*
|
||
choix des individus d'une population qui vont survivre à la
|
||
génération suivante :
|
||
- Les individus les mieux adaptés ont plus de
|
||
chance de survivre ;
|
||
- Méthode du /tournoi/ .
|
||
|
||
|
||
*** le *croisement*
|
||
|
||
Combine les gènes de deux individus pour en produire deux
|
||
nouveaux :
|
||
- croisement **1 point** : découpe des deux génome au même
|
||
endroit, puis échange ,
|
||
- croisement **2 points** : découpe chacun des génomes en trois
|
||
portions , on échange les gènes des parties médianes.
|
||
|
||
|
||
*** La *mutation*
|
||
|
||
- Mutation /aléatoire/ des caractéristiques d'un individu ;
|
||
- Réalisée avec une probabilité faible ;
|
||
- Permet de faire apparaitre dans une population de nouvelles
|
||
caractéristiques
|
||
|
||
|
||
|
||
* L'algorithme génétique
|
||
|
||
Notre algorithme travaillera avec une population constante d'une
|
||
génération à l'autre.
|
||
|
||
** Entrées
|
||
- la fonction de /fitness/ du problème ;
|
||
- le nombre d'individus $n$ pair ;
|
||
- le nombre de génération $g$ ;
|
||
- la probabilité de mutation $p$.
|
||
|
||
|
||
** L'algorithme
|
||
|
||
1. population $\leftarrow$ $n$ individus créés aléatoirement.
|
||
2. calculer pour chaque individu de population son degré
|
||
d'adaptation.
|
||
3. répéter $g$ fois
|
||
4. $\quad$ next_gen_1 $\leftarrow$ sélectionner par tournoi
|
||
$\frac{n}{2}$ individus de la population
|
||
5. $\quad$ next_gen_2 $\leftarrow$ créer $\frac{n}{2}$ individus
|
||
par croisement entre individus de population
|
||
6. $\quad$ next_gen $\leftarrow$ next_gen_1 + next_gen_2
|
||
7. $\quad$ faire muter chacun des individus de next_gen
|
||
8. $\quad$ calculer pour chaque individu de next_gen son degré
|
||
d'adaptation
|
||
9. $\quad$ meilleurs $\leftarrow$ les 5 individus les mieux adaptés de population
|
||
10. $\quad$ retirer de next_gen les 5 individus les moins adaptés
|
||
11. $\quad$ population $\leftarrow$ neilleurs + next_gen
|
||
12. solution $\leftarrow$ individu le mieux adapté de population
|
||
|
||
** Sélection par tournoi
|
||
|
||
- La sélection par tournoi dans une population de taille $n$
|
||
consiste à choisir aléatoirement deux individus de la population
|
||
pour ne garder que l'individu le mieux adapté des deux.
|
||
- On répète ce processus de sorte que chaque individu est choisi
|
||
exactement une fois. On obtient donc $\frac{n}{2}$ individus à la
|
||
fin de cette phase.
|
||
|
||
** Croisements
|
||
|
||
- Croisement à un point
|
||
- On choisit aléatoirement deux individus de la population pour
|
||
construire deux nouveaux individus
|
||
- On applique ensuite un tournoi entre les deux individus produits
|
||
pour ne garder que le meilleur
|
||
- On répète ce processus de sorte que chaque individu est choisi
|
||
exactement une fois. On obtient donc $\frac{n}{2}$ individus à la
|
||
fin de cette phase.
|
||
|
||
** Mutations
|
||
|
||
- Pour chacun des individus considérés, il y a une probabilité $p$
|
||
que chacune de ses caractéristiques mute. C'est-à-dire que la
|
||
valeur de cette caractéristique est modifiée
|
||
aléatoirement. L'individu est donc remplacé par sa /version
|
||
mutée/.
|
||
|
||
- À la fin de la population la taille de la population est la même.
|
||
|
||
|
||
* Programmation
|
||
|
||
Plusieurs classes :
|
||
- l'algorithme génétique,
|
||
- le problème traité et
|
||
- les individus.
|
||
|
||
|
||
|
||
** l'algorithme génétique
|
||
|
||
- classe `AlgoGen`.
|
||
|
||
|
||
#+begin_src python :tangle src/algo_gen.py :exports none
|
||
import random
|
||
|
||
BEST_KEPT = 5
|
||
|
||
class AlgoGen(object):
|
||
|
||
def __init__(self, problem, population_size, crossover_rate, mutation_probability):
|
||
'''
|
||
build an genetic algorithm to solve problem using a population of size population_size and a probability of muation of mutation_probability
|
||
|
||
:param problem: the problem to solve
|
||
:type problem: a Problem object
|
||
:param population_size: the size of the population (must be even)
|
||
:type population_size: int
|
||
:param mutation_probability: the mutation probability
|
||
:type mutation_probability: float
|
||
:UC: population_size must be even and mutation_probability must be 0<= and <1
|
||
'''
|
||
self.__problem = problem
|
||
self.__population_size = population_size
|
||
self.__population = []
|
||
self.__nb_crossover = crossover_rate
|
||
self.__mutation_probability = mutation_probability
|
||
|
||
def get_population(self):
|
||
return self.__population
|
||
|
||
def genesis(self):
|
||
"""
|
||
create the population
|
||
"""
|
||
pass
|
||
|
||
def evaluate_population(self, population):
|
||
"""
|
||
evalue la population passée en paramètre
|
||
:param population: (list) la population
|
||
"""
|
||
pass
|
||
|
||
|
||
def next_generation_tournament(self, population):
|
||
'''
|
||
organise un tournoi entre les individu d'une population .
|
||
chaque individu choisi une et une seule fois, dans un tournoi ,
|
||
vainqueur conservé taille résultat = 1/2 taille population
|
||
|
||
:param population: (list) la population
|
||
:return: (list) les vainqueurs du tournoi
|
||
'''
|
||
pass
|
||
|
||
|
||
|
||
def crossover(self, population):
|
||
'''Croise les individus d'une population.
|
||
|
||
Chaque individu est sélectionné une et une seule fois, on
|
||
garde le meilleur descendant à chaque fois.
|
||
|
||
taille résultat = 1/2 taille population
|
||
|
||
:param population: (list) a population
|
||
:return: (list) new individuals
|
||
'''
|
||
|
||
pass
|
||
|
||
def mutate(self, individuals):
|
||
"""
|
||
mute les individus
|
||
:param individuals: (list) une liste des individus
|
||
"""
|
||
pass
|
||
|
||
def one_round(self):
|
||
'''
|
||
applique successivement les opéréateurs
|
||
- selection
|
||
- croisement
|
||
- mutation
|
||
'''
|
||
pass
|
||
|
||
|
||
def solve(self, nb_rounds):
|
||
"""
|
||
tente d'approcher une solution en faisant muter la population
|
||
:param nb_rounds: (int) nombre de génération
|
||
"""
|
||
self.genesis()
|
||
self.evaluate_population(self.__population)
|
||
self.display_stat('INITIAL')
|
||
for step in range(nb_rounds):
|
||
self.one_round()
|
||
#self.display_population(step=step+1, number = 20)
|
||
self.display_stat(step)
|
||
#self.display_population(number = 20) # display 20 best individuals
|
||
return self.__problem.best_individual(self.__population)
|
||
|
||
|
||
def display_stat(self, step):
|
||
best = self.__problem.best_individual(self.__population)
|
||
all_scores = [ individual.get_score() for individual in self.__population ]
|
||
average = sum (all_scores) / self.__population_size
|
||
print ( ' STEP = {} -- best = {} - avg = {} '.format(step, best, average) )
|
||
|
||
|
||
def display_population(self, step = ' ', number = 0):
|
||
if number <= 0:
|
||
individuals = self.__population
|
||
else:
|
||
individuals = self.__population[:min(number, len(self.__population))]
|
||
print( ' **** STEP : {} ********* '.format(step) )
|
||
for individual in individuals :
|
||
print ( individual )
|
||
print( )
|
||
|
||
|
||
# tool function
|
||
def biased_wheel(alea,scores):
|
||
'''
|
||
return smallest i such that alea < sum(scores[k], 0 <= k < i)
|
||
CU : 0 <= alea < sum(scores)
|
||
'''
|
||
current_sum = 0
|
||
result_index = -1
|
||
while alea > current_sum :
|
||
result_index = result_index + 1
|
||
current_sum = current_sum + scores[result_index]
|
||
return result_index
|
||
#+end_src
|
||
|
||
** les problèmes
|
||
|
||
Comme indiqué, les classes pour représenter les problèmes devront
|
||
partager certaines méthodes. Nous suggérons les méthodes suivantes
|
||
comme méthodes communes aux problèmes. Evidemment, il peut exister
|
||
d'autres méthodes spécifiques à chacun des problèmes. Notamment la
|
||
manière de calculer l'adaptation d'un individu variera.
|
||
|
||
#+begin_src python :tangle src/problem_interface.py :exports none
|
||
class Problem_Interface(object):
|
||
'''
|
||
a prototype class for "problems" in genetic algorithm
|
||
|
||
an Individual class must be associated to a problem to represent the individuals
|
||
'''
|
||
|
||
def create_individual(self):
|
||
'''
|
||
create a randomly generated indidivual for this problem
|
||
|
||
:return: a randomly generated individual gfor this problem
|
||
:rtype: an Individual object
|
||
'''
|
||
pass
|
||
|
||
def evaluate_fitness(self, individual):
|
||
'''
|
||
compute the fitness of individual for this problem
|
||
|
||
:param individual: the individual to consider
|
||
:type individual: an Individual object
|
||
:return: the fitness of individual for this problem
|
||
:rtype: an Individual object
|
||
'''
|
||
pass
|
||
|
||
def sort_population(self, population):
|
||
'''
|
||
sort population from best fitted to worst fitted individuals.
|
||
Depending on the problem, it can correspond to ascending or descending order with respect to the fitness function.
|
||
|
||
side effect : population is modified by this method
|
||
|
||
:param population: the list of individuals to sort
|
||
:type population: list(Individual)
|
||
'''
|
||
pass
|
||
|
||
def best_individual(self, population):
|
||
'''
|
||
return the best fitted individual from population.
|
||
Depending on the problem, it can correspond to the individual with the highest or the lowest fitness value.
|
||
|
||
|
||
:param population: the list of individuals to sort
|
||
:type population: list(Individual)
|
||
:return: the best fitted individual of population
|
||
:rtype: an Individual object
|
||
'''
|
||
pass
|
||
|
||
def tournament(self, first, second):
|
||
'''
|
||
perform a rounament between two individuals, the winner is the most fitted one, it is returned
|
||
|
||
|
||
:param first: an individual
|
||
:type first: an Indivdual object
|
||
:param second: an individual
|
||
:type second: an Individual object
|
||
:return: the winner of the tournament
|
||
:rtype: Individual object
|
||
'''
|
||
pass
|
||
#+end_src
|
||
|
||
** les individus
|
||
|
||
Les classes pour les individus devront partager certaines
|
||
méthodes. Nous suggérons les méthodes suivantes comme méthodes
|
||
communes aux individus. Evidemment, il peut exister d'autres méthodes
|
||
spécifiques à chacun des individus, et notamment, les valeurs des
|
||
génomes vont différer.
|
||
|
||
#+begin_src python :tangle src/individual_interface.py :exports none
|
||
import random
|
||
|
||
|
||
class Individual_Interface(object):
|
||
'''
|
||
an Individual in genteic algorithm problem
|
||
|
||
the value (or genome) of an indal is a sequence (e.g string or list) of a fixed size
|
||
|
||
an individual has a fitness score
|
||
'''
|
||
|
||
|
||
|
||
def __init__(self, size):
|
||
'''
|
||
create an Individual object, its genome value is randomly built. Initially score is not set.
|
||
|
||
:param size: size of the genome
|
||
:type size: int
|
||
'''
|
||
self.__score = None
|
||
self.__size = size
|
||
self.__value = self.init_value()
|
||
|
||
def copy(self):
|
||
'''
|
||
build a copy of self, the genome is a copy of self's genome
|
||
|
||
:return: a new Individual which is a "clone" of self
|
||
:rtype: an Individual object
|
||
'''
|
||
return None
|
||
|
||
def get_size(self):
|
||
'''
|
||
:return: the size of self's genome
|
||
:rtype: int
|
||
'''
|
||
|
||
return self.__size
|
||
|
||
def get_score(self):
|
||
'''
|
||
:return: the fitness score of self
|
||
:rtype: number
|
||
'''
|
||
return self.__score
|
||
|
||
def set_score(self, new_score):
|
||
'''
|
||
change the fitness score of self
|
||
|
||
:param new_score: the new fitness score
|
||
:type new_score: number
|
||
'''
|
||
self.__score = new_score
|
||
|
||
def __str__(self):
|
||
return "{} ({})".format(self.__value, self.__score)
|
||
|
||
def __repr__(self):
|
||
return self.__str__()
|
||
|
||
def get_value(self):
|
||
'''
|
||
:return: the genome of self
|
||
:rtype: sequence
|
||
'''
|
||
return self.__value
|
||
|
||
def set_value(self, new_value):
|
||
'''
|
||
change the genome value of self
|
||
|
||
:param new_score: the new genome value
|
||
:type new_score: sequence
|
||
'''
|
||
self.__value = new_value
|
||
|
||
def init_value(self):
|
||
'''
|
||
randomly initialize the genome value of self
|
||
'''
|
||
return None
|
||
|
||
def evaluate(self,problem):
|
||
'''
|
||
set the fitness score with the fitness computed by problem for self
|
||
|
||
:param problem: the problem
|
||
:type problem: a Problem object
|
||
'''
|
||
self.set_score(problem.evaluate_fitness(self))
|
||
|
||
def cross_with(self, other):
|
||
'''
|
||
perform a 1 point crossover between self and other, two new built individuals are returned
|
||
|
||
:param other: the individual to croww with
|
||
:type other: an Idnividual object
|
||
:return: the two new Individuals built by 1 point crossover operation
|
||
:rtype: 2-uple of individuals
|
||
'''
|
||
pass
|
||
|
||
|
||
|
||
def mutate(self, probability):
|
||
'''
|
||
apply mutation operation to self : each element of the geome sequence is randomly changed with given probabiliy
|
||
|
||
side effect : self's genome is modified
|
||
|
||
:param probability: the probability of mutation for every gene
|
||
:type probability: float
|
||
:UC: probability in [0,1[
|
||
'''
|
||
pass
|
||
|
||
def mutate_gene(self, gene):
|
||
pass
|
||
#+end_src
|
||
|
||
|
||
* Problème 1 : Calcul de la valeur maximale d'une fonction
|
||
|
||
Problème : trouver dans un intervalle $[x_\mbox{min};x_\mbox{max}]$
|
||
une valeur de $x$ pour laquelle $f(x)$ est maximale.
|
||
|
||
|
||
|
||
* Problème 2 : Recherche d'un message "secret"
|
||
|
||
** Description
|
||
Cette fois, il ne s'agit pas à proprement parler d'un problème à
|
||
résoudre, puisqu'on en connaît la solution au départ... Ce second
|
||
exemple a pour objectif d'illustrer que les algorithmes génétiques
|
||
peuvent trouver une solution dans un très grand espace de recherche.
|
||
|
||
** Problème
|
||
Le problème est celui de faire trouver une chaîne de caractères à un
|
||
algorithme génétique. On suppose cette chaîne construite à partir du
|
||
caractère espace et des 26 lettres de l'alphabet (minuscules et non
|
||
accentuées). Si elle est le longueur 30, il existe alors *27^30*
|
||
chaînes possibles.
|
||
|
||
Nous allons donc vérifier qu'en explorant qu'une petite partie de
|
||
toutes ces possibilités, un
|
||
algorithme génétique est capable de trouver l'une d'entre elles.
|
||
|
||
|
||
Par exemple, nous allons considérer la chaine
|
||
*secret* = ~'ceci est un secret a retrouver'~.
|
||
|
||
|
||
* Problème 3 : Recherche d'un chemin dans un labyrinthe
|
||
|
||
** Labyrinthe
|
||
|
||
#+begin_example
|
||
7;7
|
||
+-+-+-+-+-+-+-+
|
||
| | |
|
||
+ + +-+ + +-+ +
|
||
| | | | |
|
||
+ + +-+ + + + +
|
||
| | | | |
|
||
+ +-+ +-+-+ + +
|
||
| | | |
|
||
+ + +-+-+ +-+-+
|
||
| | | |
|
||
+ +-+-+ + +-+ +
|
||
| | | | |
|
||
+ + + +-+-+ +-+
|
||
| | |
|
||
+-+-+-+-+-+-+-+
|
||
|
||
#+end_example
|
||
|
||
** Problème
|
||
Le problème consiste à trouver dans un tel labyrinthe de taille *N*
|
||
(ici *N=7x7=49*) donné, un chemin entre la case d'entrée située en
|
||
haut à gauche et celle de sortie située en bas à droite.
|
||
|
||
Pour simplifier on considère que les largeur et hauteur du
|
||
labyrinthe sont identiques.
|
||
|
||
|
||
** Matériel
|
||
|
||
- une classe ~Maze~ permettant de gérer les labyrinthes (fichier ~maze.py~);
|
||
- des exemples de labyrinthe (fichier ~maze.zip~)
|
||
|
||
|
||
* Problème 4 : le TSP
|
||
|
||
Utiliser un algorithme génétique pour approcher une solution du
|
||
Problème du voyageur de commerce.
|