Les algorithmes génétiques s'inspirent de la théorie de l'évolution, initiée par Charles Darwin au XIXeme siècle.
Dans cette théorie, une population d'individus évolue grâce au mécanisme de la reproduction sexuée. Les individus les plus adaptés à leur milieu se reproduisent plus que les autres, favorisant les caractères les plus adaptés. Ainsi une girafe avec un cou plus long que les autres aura accès à plus de nourriture, et aura donc plus de chances de survivre et de se reproduire. Ses descendants auront un cou plus long, et en moyenne la population de girafe aura un cou plus long.
Au niveau de l'ADN, la recombinaison de l'ADN des deux parents (lors de la reproduction) permet de générer différentes combinaisons de gènes. Il y a des chances qu'en recombinant l'ADN de deux parents bien adaptés, l'individu généré soit encore mieux adapté.
Exemple :
Parent 1 | Parent 2 |
---|---|
Cou : long | Cou : court |
Pattes : courtes | Pattes : longues |
adaptation : moyenne | adaptation : moyenne |
peut donner à la génération suivante :
Fils 1 | Fils 2 | Fils 3 |
---|---|---|
Cou : court | Cou : court | Cou : long |
Pattes : courtes | Pattes : longues | Pattes : longues |
adaptation : mauvaise | adaptation : moyenne | adaptation : bonne |
L'idée a été proposée la premiere fois par John Holland au début des années 1970, qu'il a d'ailleurs utilisée pour le problème du voyageur de commerce.
Il s'agit de résoudre un problème d'optimisation d'une combinaison de paramètres dépendants les uns des autres. Ces paramètres peuvent prendre plusieurs valeurs prédéfinies, de la même manière que des gènes peuvent prendre plusieurs expressions (pattes longues ou pattes courtes, par exemple). Une solution particulière au problème (une solution approchée) est représentée par l'ensemble des valeurs prises pour chaque paramètre, qui constitue en quelque sorte l'ADN de cette solution.
Quant à l'adaptation à l'environnement, elle est représentée par une évaluation chiffrée indiquant la pertinence de la solution.
Par exemple pour le PVC : un individu est un trajet, son évaluation est la longueur totale de ce trajet, un gène est la ville où l'on passe
à une certaine position dans le parcours, et l'ADN est la liste des villes dans l'ordre du parcours.
Parmi un ensemble de solutions approchées (la population), on sélectionne alors deux bonnes solutions
et on les recombine pour en produire une nouvelle.
D'autre part, on élimine les solutions les moins adaptées.
En répétant ce processus, l'adaptation de la population augmente, et donc on converge vers la solution du problème.
Il s'agit bien sûr d'une adaptation informatique, il ne faut pas chercher une similitude plus poussée avec les "lois de la nature". Entre autres parce dans la nature, le cerveau joue un grand rôle, et que l'environnement est changeant. Donc une solution moyenne mais adaptable à plusieurs environnements est plus intéressante qu'une solution très adaptée à un environnement particulier.
De manière plus formelle, voici un algorithme génétique de base :
Pour chaque Individu dans Groupe Initialiser Individu FinPour Pour nombre_d'itérations Parent A = Sélection_d'un_Individu (Groupe) Parent B = Sélection_d'un_Individu (Groupe) Fils = Recombinaison (Parent A, Parent B) Si hasard > pourcentage Alors Appliquer_une_mutation_à Fils FinSi Optimiser Fils // Optionnel Evaluer Fils Si Fils est_accepté_dans Groupe Alors Réinsérer Fils dans Groupe FinSi FinPour
Il existe énomément de variantes et de paramétrages qui affectent grandement les résultats.
Voyons maintenant plus en détail chaque phase :
Comment choisir les parents pour la reproduction ? Il existe plusieurs possibilités :
Il semble qu'une stratégie élitiste soit plus efficace au démarrage de l'algorithme, puis au fur et à mesure que l'on se rapproche de l'optimum, chaque individu a à peu près autant de chances d'être précurseur d'une meilleure solution.
Pour tspgen 0.32 j'ai utilisé une stratégie élitiste inspirée de la "roulette wheel" : On considère la sélection d'un individu comme le lancement d'une bille sur une roulette. Chaque individu possède plusieurs cases dans la roulette. Les meilleurs individus possèdent plus de cases et sont donc sélectionnés plus souvent.
Pour cela les individus sont classés selon leur adaptation (premier individu = longueur de trajet la plus courte).
Je crée un tableau d'entiers, qui comporte autant d'éléments que d'individus dans le groupe.
Chaque entier du tableau prend une valeur croissante entre 1 et RAND_MAX. L'intervalle entre les chiffres du tableau est plus grand au début
et plus petit à la fin. Comme la fonction rand () renvoie un entier au hasard entre 1 et RAND_MAX il suffit de tirer un chiffre
au hasard et de le comparer au chiffre du tableau pour connaitre l'index de l'individu sélectionné.
Pour que chaque individu ait la même probabilité d'être séléctionné, il suffirait de positionner les chiffres du tableau à un intervalle de
RAND_MAX / Nb_individus.
Voir le code source des fonctions Group::InitSelectionArray () et Group::GetRandSelectionArrayIndex ()
C'est la fonction principale de l'algorithme génétique. La recombinaison (ou reproduction ou crossover) consiste à créer un individu à partir de deux individus parents. Il existe presque autant d'algorithmes de recombinaison que d'implémentations d'algorithmes génétiques...
Un exemple de crossover réussi (à comparer avec l'exemple des girafes) :
Parent 1 |
Parent 2 |
Fils 1 |
Je vais décrire ici un des plus simples, le crossover à 1 point (c'est loin d'être le plus efficace mais il a le mérite d'être assez simple à coder).
Il s'agit de recopier dans le fils une partie du parent 1, jusqu'à une "cassure", puis ensuite la partie correspondante du parent 2. La difficulté réside dans le fait que les villes ne doivent pas être répétées. Si la ville a déjà été prise, on passe à la ville suivante dans l'ordre du parent 2.
Exemple :1-2-3-4-5-6-7-8-9-10
2-7-1-4-10-6-9-8-3-5
1-2-3-4-10-6-9-8-5-7
Une variante de ce crossover est le crossover à 2 points (un peu plus efficace) : on introduit une deuxième cassure et le parent 2 n'est recopié qu'entre ces deux cassures.
Ces deux algorithmes ont un défaut pour le PVC, c'est qu'ils ne respectent pas toujours la règle suivante :
- si une arête (chemin entre 2 villes) se trouve dans les deux trajets parents, il faut qu'elle se trouve dans le trajet fils
Ce problème a été en partie résolu par le crossover OX (entres autres) [que je présenterai ici prochainement].
Les crossovers peuvent aussi utiliser des informations heuristiques, comme le crossover KFP dans tspgen 0.32 (qui utilise les villes les plus proches). Cet algo a été décrit par Karoly F. Pal dans un papier de 1993 (je n'en ai qu'un exemplaire papier, il n'est pas dispo sur le net à ma connaissance).
Il s'agit d'une modification (plus ou moins aléatoire) du code génétique d'un individu. Cela permet de sortir des minimums locaux, grâce à une perturbation, un peu à la manière du recuit simulé.
Exemples de mutations pour le PVC :
Cette phase est à mon avis très importante et pas assez mise en valeur dans les différentes docs sur les algos génétiques.
L'optimisation est l'utilisation d'une méthode de recherche locale (par exemple 2-opt ou 3-opt), appliquée à chaque individu lors
de sa création. Chaque individu est un minimum local, on ne risque donc pas de passer à côté d'une bonne solution.
Il faut savoir que la plupart du temps de calcul est passée dans ces optimisations locales (au moins dans tspgen, mais je pense que c'est la même chose dans tous les algorithmes génétiques ou les autres métaheuristiques comme le recuit simulé ou la méthode de la colonie de fourmis). Sans cette optimisation, le résultat serait assez médiocre. Mais on ne peut pas dire que c'est l'optimisation qui fait tout le travail ! Le grand avantage des algorthmes génétiques est qu'ils fonctionnent quand même EN PLUS de l'optimisation locale. L'algorithme génétique est donc responsable de la partie la plus difficile du calcul.
Comment réinsérer le fils dans le groupe ? Les solutions sont :
Ce qui est certain est qu'il ne faut pas supprimer le meilleur individu du groupe, sous peine de voir l'adaptation globale diminuer !