đŸ‘ïžâ€đŸ—šïž Coup d’oeil sur l’algorithme gĂ©nĂ©tique

Initiation Ă  l’algorithme gĂ©nĂ©tique : prĂ©sentation des principes et petit cas pratique

Mlachahe Said Salimo
Wanabilini
11 min readDec 5, 2022

--

En recherche opérationnelle, les méthodes de résolution exactes (comme le simplexe) étant de complexité exponentielle, donc relativement chronophage, parfois il plus judicieux de faire appel à des méthodes heuristiques pour des problÚmes difficiles.

  • Un heuristique est un algorithme qui permet afin de trouver une solution approchĂ©e, pas nĂ©cessairement optimale, pour un problĂšme d’optimisation difficile ; dans un dĂ©lai de temps raisonnable.

Les heuristiques sont dĂ©pendantes du problĂšme Ă  rĂ©soudre, toutefois des heuristiques plus poussĂ©es ont Ă©tĂ© mises au point — un niveau d’abstraction plus Ă©levĂ© que les heuristiques puisqu’il s’agit de mĂ©thode pouvant s’adapter Ă  un problĂšme donnĂ© — et ont donnĂ©es naissance Ă  une nouvelle famille d’algorithmes : les mĂ©ta-heuristiques.

  • L’idĂ©e principale des mĂ©taheuristiques est d’explorer l’espace des solutions en essayant de converger vers l’optimum global (la meilleure solution dans l’ensemble) ; sans ĂȘtre « piĂ©gĂ© » par un optimum local (la meilleure solution dans une zone restreinte).
Les mĂ©taheuristiques (M) tentent de trouver l’optimum global (G) d’un problĂšme d’optimisation difficile (avec des discontinuitĂ©s — D — , par exemple), sans ĂȘtre piĂ©gĂ© par les optima locaux (L).

GĂ©nĂ©ralement de nature stochastiques et itĂ©ratifs, ces algorithmes se veulent ĂȘtre des mĂ©thodes gĂ©nĂ©riques pouvant optimiser une large gamme de problĂšmes diffĂ©rents :

  • Les plus simples : ProblĂšme du voyageur, problĂšme du sac Ă  dos, etc.
  • Comme les plus compliquĂ©s : Choix optimal des paramĂštres du modĂšle K-Nearest Neighbors (KNN) ou K-Means, rĂ©duction de la complexitĂ© des rĂ©seaux de neurones, etc.

Les mĂ©taheuristiques sont souvent inspirĂ©es par des systĂšmes naturels, et sont de plus en plus hybridĂ©es avec d’autres mĂ©thodes de recherche opĂ©rationnelle.

Parmi les principales métaheuristiques utilisées pour résoudre des problÚmes à variables discrÚtes, on retrouve les algorithme génétiques.

Algorithme génétique : principes et fonctionnement

Inspiration de la nature

Les algorithmes gĂ©nĂ©tiques (AG) sont de la famille des algorithmes Ă©volutionnaires ; des algorithmes d’optimisation stochastique fondĂ©s sur les mĂ©canismes de la sĂ©lection naturelle et de la gĂ©nĂ©tique.

Les principes de base des AG, dĂ©veloppĂ©s par le scientifique amĂ©ricain John Henry Holland dans les annĂ©es 1970, s’inspirent directement des travaux du naturaliste anglais Charles Darwin. En effet, la thĂ©orie de l’évolution, dĂ©veloppĂ©e dans le cĂ©lĂšbre ouvrage De l’origine des espĂšces de Darwin, postule que le principal moteur de l’évolution est le processus de sĂ©lection naturelle.

Illustration de la théorie de Darwin

Cette notion, que l’on qualifie parfois comme la survie du plus apte (survival of the fittest, en anglais), repose sur trois principes : le principe de variation, le principe d’adaptation et le principe d’hĂ©rĂ©ditĂ©. :

  • Le principe de variation : Chaque individu au sein d’une population est unique. Ces diffĂ©rences, plus ou moins importantes, vont ĂȘtre dĂ©cisives dans le processus de sĂ©lection.
Chez les girafes, la couleur de la peau, le nombre de taches ou la taille du cou sont des caractĂšres pour lesquels il existe de multiples variations ou traits.
  • Le principe d’adaptation : Les individus ayant les variations les plus favorables par rapport Ă  leur environnement sont plus adaptĂ©s et atteignent plus facilement l’ñge adulte. Ceux ayant une meilleure capacitĂ© de survie pourront donc se reproduire davantage.
Chez les ancĂȘtres de la girafe, les individus dotĂ©s d’un cou plus long ont eu un avantage sur les autres. Cette variation a affecter positivement la survie de l’espĂšce dans son environnement
  • Le principe d’hĂ©rĂ©ditĂ© : Les caractĂ©ristiques et variations des individus sont hĂ©rĂ©ditaires, c’est-Ă -dire hĂ©ritĂ©es des parents et transmises aux descendants. Ainsi, aprĂšs plusieurs gĂ©nĂ©rations, on verra les variations favorables se retrouver chez tous les individus de la population et la frĂ©quence des gĂšnes dĂ©savantageux diminuer jusqu’à Ă©ventuellement disparaĂźtre.
Chez les girafes, lors de la reproduction, les individus transmettent leurs gĂšnes aux descendants

Le fonctionnement de base de l’algorithme gĂ©nĂ©tique

À l’image de la sĂ©lection naturelle, le fonctionnement des algorithmes Ă©volutionnaires est axĂ© sur un processus itĂ©ratif que nous pouvons de diviser en cinq phases :

1. Initialisation
2. Évaluation
3. SĂ©lection
4. Croisement
5. Mutation
Ensuite, il y a un Retour Ă  la phase d’évaluation jusqu’à l’arrĂȘt de l’algorithme.

Fonctionnement d’un algorithme gĂ©nĂ©tique simple

GrossiĂšrement, les AG se basent au dĂ©part sur une population de solutions candidates appelĂ©es parfois individus, celles-ci comprennent des propriĂ©tĂ©s et elles peuvent ĂȘtre sujettes Ă  des transformations gĂ©nĂ©tiques (mutation, croisement par exemple) qui vont faire Ă©voluer la population de gĂ©nĂ©ration en gĂ©nĂ©ration, jusqu’à la gĂ©nĂ©ration qui contient les meilleures solutions.

Pour mieux apprĂ©hender le fonctionnement des AG, quoi de mieux qu’un cas pratique ?

Cas pratique : Maximisation de la somme des gùnes d’un octet

Objectif du cas pratique

Pour ce cas pratique, nous allons avoir au dĂ©part une liste alĂ©atoire d’individus en reprĂ©sentation binaire. Nous travaillerons avec une liste contenant 10 individus.

Liste typique d’octets

L’objectif va ĂȘtre d’utiliser un algorithme gĂ©nĂ©tique afin d’avoir des individus dont la somme des gĂšnes est maximisĂ©.

Par exemple, la somme des gùnes de l’individu 10101000 vaut 3

Bien que ce problÚme soit trivial, le but de ce cas pratique est de comprendre pas à pas les notions, comment utiliser et implémenter les algorithmes génétiques.

Initialisation de la population

Pour commencer, il est important de définir quelques notions de bases :

  • La population est l’ensemble des solutions envisageables.
  • L’individu reprĂ©sente une solution.
  • Le gĂšne est une caractĂ©ristique, une particularitĂ© de la solution.

Dans notre problĂšme, cela Ă©quivaut Ă  cela :

Identification de la population, d’un individu et d’un gùne

Pour notre problĂšme, Ă©tant donnĂ© qu’un octet est composer de 8 bits, nous reprĂ©senterons un individu par un array de taille 8, rempli de 1 et de 0.

import numpy as np
import random
# Generate randomly an individual
def generate_individual():
individu = np.random.randint(0,2, size=(1,8))
return individu

generate_individual()

# Output : array([[1, 1, 0, 1, 1, 1, 0, 0]])

DĂšs lors, nous pouvons gĂ©nĂ©rer alĂ©atoirement la population d’octets sur laquelle nous allons travailler.

# Generate a population of individual generated randomly
def generate_population(size):
pop = np.empty(shape=[0, 8])
for i in range(size):
pop = np.vstack([pop, generate_individual()])
return pop

# Output : array([[0., 1., 0., 0., 1., 1., 0., 0.],
# [0., 0., 1., 1., 0., 0., 1., 1.],
# ...,
# [0., 0., 0., 0., 1., 0., 0., 0.]])

Evaluation de la population

Maintenant que notre population est gĂ©nĂ©rĂ©, il faut l’évaluer. Nous allons attribuer Ă  chaque individu un score (aussi appelĂ© fitness). En effet, les individus ayant une bonne qualitĂ© seront ceux avec un score Ă©levĂ©. Ils auront plus de chance d’ĂȘtre sĂ©lectionnĂ©s et donc il y a plus de chance que la populati Pour notre problĂšme, c’est relativement simple, nous dĂ©finissons la fitness comme la somme des variables (0 ou 1) de l’individu. on suivante hĂ©rite de leur matĂ©riel gĂ©nĂ©tique.



Pour notre problĂšme, c’est relativement simple, nous dĂ©finissons la fitness comme la somme des variables (0 ou 1) de l’individu.
# Attribute a score to each individual of the population : sum of their genes
def compute_fitness(population):
matriceScores = population.sum(axis=1)
return matriceScores

# Work on population : Sort the population in ascending order
def evaluate_population(population):
pop = population.copy() # get the population
listScores = compute_fitness(pop) # get the list of scores
listScores = listScores.argsort(axis=0) # sort the list of scores
population_evaluated = pop[listScores] # put the mask in population to sort population
return population_evaluated

Opérateurs génétique

Pour la suite, il est important de comprendre qu’il y a trois opĂ©rateurs d’évolution dans les algorithmes gĂ©nĂ©tiques :

  • La sĂ©lection qui permet de choisir les individus les mieux adaptĂ©s.
  • Le croisement qui permet de mĂ©langer des individus choisis par la reproduction de certaines de leurs propriĂ©tĂ©s.
  • La mutation qui permet d’altĂ©rer / faire varier alĂ©atoirement les propriĂ©tĂ©s d’un individu.

Opérateur de sélection sur la population

La sĂ©lection consiste Ă  choisir les individus les mieux adaptĂ©s (i.e. avec les meilleurs fitness) afin d’avoir une population de solution la plus proche de converger vers l’optimum global. Cet opĂ©rateur est l’application du principe d’adaptation de la thĂ©orie de Darwin.

Il existe plusieurs techniques de sélection. Voici les principales utilisées :

  • SĂ©lection par rang : on choisit toujours les individus possĂ©dant les meilleurs scores d’adaptation (fitness).
  • SĂ©lection par tournoi : on sĂ©lectionne proportionnellement des paires d’individus, puis choisit parmi ces paires l’individu qui a le meilleur score d’adaptation.
  • SĂ©lection uniforme : on sĂ©lectionne alĂ©atoirement, uniformĂ©ment et sans intervention de la valeur d’adaptation.

Pour notre problÚme, les individus de la population ont été évaluées trÚs facilement ; ceux ayant un score élevé ont été classé en fin du tableau. Nous opterons alors pour une sélection par rang, et nous sélectionnerons les meilleurs individus de la population en indexant le tableau par la fin.

# Work on population : Select a percentage of the population
def select_population(population, percent):
pop = population.copy() # get the population

howmany = int(round(len(pop)*(percent))) # the number of individu to select
population_selected = pop[-howmany:]
return population_selected

Opérateur de croisement sur la population

Le croisement (ou crossing-over en anglais), est le rĂ©sultat obtenue lorsque deux chromosomes partage leurs particularitĂ©s. Cet opĂ©rateur est l’application du principe d’hĂ©rĂ©ditĂ© de la thĂ©orie de Darwin, et permet le brassage gĂ©nĂ©tique de la population.

Il existe plusieurs techniques de croisement. Voici les principales utilisées :

  • Le croisement simple : on fusionne les particularitĂ©s de deux individus Ă  partir d’un point de coupure, afin d’obtenir un ou deux enfants.”
Croisement simple — Un point de coupure
  • Le croisement multiple : mĂȘme principe que le prĂ©cĂ©dant, sauf qu’il y a plusieurs points de coupure.
Croisement double — Deux points de coupures
  • Le croisement uniforme : on fusionne les particularitĂ©s de deux individus Ă  partir d’un masque de croisement gĂ©nĂ©rĂ© alĂ©atoirement, afin d’obtenir un ou deux enfants.

Pour notre problÚme, nous opterons pour un croisement simple ; les individus-enfants hériterons de la moitié des caractéristiques respectives des parents.

# Work on individual : Create an individual crossed simply with 2 individuals 
def crossover(parent1, parent2):
genes1, genes2 = parent1[:4], parent2[-4:]
child = np.hstack([genes1, genes2])
return child

DĂšs lors, nous implĂ©mentons une fonction permettant d’appliquer l’opĂ©rateur de croisement dans une population et pouvoir en faire croiser une partie (un pourcentage).

# Work on population : Cross a percentage (taken as a parameter) of the population
def cross_population(population, percent):
pop = population.copy() # get the population

children = np.empty(shape=[0, 8])
howmany = int(round(len(pop)*(percent))) # the number of child to create
for k in range(howmany):
ind1 = random.choice(pop)
ind2 = random.choice(pop)
# create the child
child = crossover(ind1, ind2)
# store the child
children = np.vstack([children, child])

# Add children to population
population_crossed = np.vstack([pop, children])
return population_crossed

Opérateur de mutation sur la population

La mutation consiste Ă  altĂ©rer un gĂšne dans un chromosome (individu) selon un facteur de mutation. Ce facteur est soit la probabilitĂ© qu’une mutation soit effectuĂ©e sur un individu, soit le pourcentage d’individus mutĂ©s dans une population.

Cet opĂ©rateur est l’application du principe de variation de la thĂ©orie de Darwin et permet, par la mĂȘme occasion, d’éviter une convergence prĂ©maturĂ©e de l’algorithme vers un extremum local.

Il existe plusieurs techniques de mutation. Mais pour notre cas simple nous n’utiliserons que celle-ci :

  • La mutation alĂ©atoire : consiste Ă  modifier une des particularitĂ©s dĂ©terminĂ©es alĂ©atoirement d’un individu

Pour notre problĂšme, nous opterons pour cette technique ; lorsque la valeur du gĂšne Ă  muter est Ă©gale Ă  1 alors elle est inversĂ©e Ă  0 et lorsqu’elle est Ă©gale Ă  0 alors elle est inversĂ©e Ă  1.

# Work on individual : Mutate an individual / randomly change a bit
def mutation(individu):
a = random.randrange(len(individu)) # choose a random gene to mutate
if individu[a]==0:
individu[a]=1
else:
individu[a]=0
return individu

DĂšs lors, nous implĂ©mentons une fonction permettant d’appliquer l’opĂ©rateur mutation dans une population et pouvoir en faire muter une partie (un pourcentage).

# Work on population : Mutate a percentage of the population
def mutate_population(population, percent):
pop = population.copy() # get the population

howmany = int(round(len(pop)*(percent))) # the number of individu to mutate
for k in range(howmany):
i = random.randrange(pop.shape[0]) # choose a random individu in population to mutate
individu = pop[i]
mutant = mutation(individu) # mutate the individu
pop[i] = mutant # replace individu by mutant

return pop

Création de la premiÚre génération

Nous testons rapidement nos fonctions avec en paramĂštres :
- Taille de la population : 6
- Part d’individus sĂ©lectionnĂ©s dans la population : 50%
- Part d’enfants croisĂ©s obtenus par rapport Ă  la population : 100%
- Part d’individus mutĂ©s chez les enfants : 40%

Tout d’abord, une population est initialisĂ©e :

population = generate_population(6)

Ensuite, cette population est évaluée :

population = evaluate_population(population)

Puis, les meilleurs individus de cette population sont sélectionnés :

population = select_population(population, 0.5)

Ces derniers vont ĂȘtre croisĂ©es, et vont faire un nombre dĂ©fini d’enfants. Les voici :

children = cross_population(population, 1.0) //// children = mutate_population(children, 0.4)

Et enfin, ces enfants vont ĂȘtre regrouper avec leurs magnifiques parents pour constituer une population finale

population = np.vstack([population, children])

Voilà une génération de crée ! Nos fonctions marche trÚs bien.

RĂ©solution du problĂšme par l’algorithme gĂ©nĂ©tique

DĂ©sormais, il ne reste plus qu’à mettre les lignes de codes prĂ©cĂ©dentes dans une boucle itĂ©rative afin de simuler le renouvellement de population au fil de N gĂ©nĂ©ration.

def algorithme_genetique(nbPopulationStart, numberOfGeneration, percentSelected, percentCrossed, percentMutated, aff):
# Initialisation
population = generate_population(nbPopulationStart)
print('Initial population\n', population) if aff>=1 else None

for t in range(numberOfGeneration):
print('\n#########################################################') if aff==2 else None
print('######################################### GENERATION :', t) if aff==2 else None
# Evaluation
population = evaluate_population(population)
print('\nEvaluation :\n', population) if aff==2 else None
# Selection
population = select_population(population, percentSelected)
print('\nSelection :\n', population) if aff==2 else None
# Crossover
children = cross_population(population, percentCrossed)
print('\nCrossover - Children :\n', children) if aff==2 else None
# Mutation
children = mutate_population(children, percentMutated)
print('\nMutation - Children :\n', children) if aff==2 else None
# store the child
population = np.vstack([population, children])
print('\nSelection :\n', population) if aff==2 else None

print('\n#########################################################') if aff==2 else None
print('>>>> Result :\n', population) if aff>=1 else None
return population

Ainsi, pour rĂ©soudre le problĂšme initiale, il suffit de lancer notre algorithme en choisissant comme paramĂštres d’entrĂ©es :
- Taille de la population : 10
- Nombre de génération : 3
- Part d’individus sĂ©lectionnĂ©s dans la population : 50%
- Part d’enfants croisĂ©s obtenus par rapport Ă  la population : 100%
- Part d’individus mutĂ©s chez les enfants : 30%

population = algorithme_genetique(10, 5, 0.5, 1, 0.3, 1)

On constate que cette population final est composĂ© majoritairement d’individus dont la somme des gĂšnes maximisĂ©e, c’est-Ă -dire composĂ© de bonnes solutions. Il a fallu seulement 3 gĂ©nĂ©ration Ă  notre algorithme pour rĂ©soudre ce problĂšme.

VoilĂ  une brĂšve introduction Ă  l’algorithme gĂ©nĂ©tique, mais il faut garder en tĂȘte qu’en dehors du modĂšle que je viens de prĂ©senter, il existe plusieurs variĂ©tĂ©s de modĂšles d’implĂ©mentation de l’AG pour avoir des solutions qui se rapprochent beaucoup plus de l’optimum.

Mlachahe SAID SALIMO.

Sources et ressources complémentaires :
- Sélection naturelle, la page Wikipédia
- Darwin et la sélection naturelle, un cours du site alloprof.qc.ca
- Optimisation combinatoire, un article du site complex-systems-ai.com
- Maxima and minima, la page Wikipédia
- Algorithme génétique, la page Wikipédia
- Algorithme gĂ©nĂ©tique, le site de l’exposĂ© de Thomas LEROUX
- Algorithme génétique, un article du site ledatascientist.com
- Les techniques algorithmiques de l’IA | Les algorithmes Ă©volutionnaires, article du site ajcact.org
- Présentation des algorithmes génétiques et de leurs applications en
Ă©conomie
, un article de Thomas Vallée et Murat Yildizoglu
- Genetic Algorithms — Quick Guide, un article du site tutorialspoint.com

--

--

Mlachahe Said Salimo
Wanabilini

French Student in Computer Science for Health. J'Ă©cris mes notes ici + J'Ă©cris pour apprendre, pas parce que je sais !