Algorithme génétique simple à partir de zéro en Python
L'algorithme génétique est un algorithme d'optimisation globale stochastique.
Il s’agit peut-être de l’un des algorithmes d’inspiration biologique les plus populaires et les plus connus, avec les réseaux de neurones artificiels.
L'algorithme est un type d'algorithme évolutif et effectue une procédure d'optimisation inspirée de la théorie biologique de l'évolution au moyen de la sélection naturelle avec une représentation binaire et des opérateurs simples basés sur la recombinaison génétique et les mutations génétiques.
Dans ce tutoriel, vous découvrirez l'algorithme d'optimisation de l'algorithme génétique.
Après avoir terminé ce tutoriel, vous saurez :
- L'algorithme génétique est un algorithme d'optimisation stochastique inspiré de l'évolution.
- Comment implémenter l'algorithme génétique à partir de zéro en Python.
- Comment appliquer l'algorithme génétique à une fonction objectif continue.
Démarrez votre projet avec mon nouveau livre Optimization for Machine Learning, comprenant des tutoriels pas à pas et les fichiers code source Python pour tous exemples.
Présentation du didacticiel
Ce didacticiel est divisé en quatre parties ; ils sont:
- Algorithme génétique
- Algorithme génétique à partir de zéro
- Algorithme génétique pour OneMax
- Algorithme génétique pour l'optimisation continue des fonctions
Algorithme génétique
L'algorithme génétique est un algorithme d'optimisation de recherche global stochastique.
Elle s'inspire de la théorie biologique de l'évolution par sélection naturelle. Plus précisément, la nouvelle synthèse qui combine une compréhension de la génétique avec la théorie.
Les algorithmes génétiques (algorithme 9.4) s’inspirent de l’évolution biologique, où les individus les plus en forme sont plus susceptibles de transmettre leurs gènes à la génération suivante.
— Page 148, Algorithmes d'optimisation, 2019.
L'algorithme utilise des analogues d'une représentation génétique (chaînes de bits), de fitness (évaluations de fonctions), de recombinaison génétique (croisement de chaînes de bits) et de mutation (retournement de bits).
L'algorithme fonctionne en créant d'abord une population d'une taille fixe de chaînes de bits aléatoires. La boucle principale de l'algorithme est répétée pendant un nombre fixe d'itérations ou jusqu'à ce qu'aucune amélioration supplémentaire ne soit constatée dans la meilleure solution sur un nombre donné d'itérations.
Une itération de l’algorithme est comme une génération évolutive.
Tout d’abord, la population de chaînes de bits (solutions candidates) est évaluée à l’aide de la fonction objectif. L'évaluation de la fonction objectif pour chaque solution candidate est considérée comme l'adéquation de la solution, qui peut être minimisée ou maximisée.
Ensuite, les parents sont sélectionnés en fonction de leur condition physique. Une solution candidate donnée peut être utilisée comme parent zéro ou plusieurs fois. Une approche de sélection simple et efficace consiste à tirer au hasard k candidats dans la population et à sélectionner le membre du groupe ayant la meilleure forme physique. C'est ce qu'on appelle la sélection de tournoi où k est un hyperparamètre et défini sur une valeur telle que 3. Cette approche simple simule un système de sélection proportionné à la condition physique plus coûteux.
Lors de la sélection en tournoi, chaque parent est le plus apte parmi k chromosomes choisis au hasard dans la population.
— Page 151, Algorithmes d'optimisation, 2019.
Les parents sont utilisés comme base pour générer la prochaine génération de points candidats et un parent pour chaque position dans la population est requis.
Les parents sont ensuite réunis par paires et utilisés pour créer deux enfants. La recombinaison est effectuée à l'aide d'un opérateur de croisement. Cela implique de sélectionner un point de partage aléatoire sur la chaîne de bits, puis de créer un enfant avec les bits jusqu'au point de partage du premier parent et du point de partage jusqu'à la fin de la chaîne du deuxième parent. Ce processus est ensuite inversé pour le deuxième enfant.
Par exemple les deux parents :
- parent1=00000
- parent2=11111
Peut donner naissance à deux enfants croisés :
- enfant1=00011
- enfant2=11100
C'est ce qu'on appelle le croisement en un point, et il existe de nombreuses autres variantes de l'opérateur.
Le croisement est appliqué de manière probabiliste pour chaque paire de parents, ce qui signifie que dans certains cas, les copies des parents sont considérées comme les enfants au lieu de l'opérateur de recombinaison. Le crossover est contrôlé par un hyperparamètre défini sur une valeur élevée, telle que 80 % ou 90 %.
Le crossover est la caractéristique distinctive de l’algorithme génétique. Cela implique de mélanger et de faire correspondre les parties de deux parents pour former des enfants. La manière dont vous procédez à ce mélange et à cette correspondance dépend de la représentation des individus.
— Page 36, Essentiels de la métaheuristique, 2011.
La mutation implique d'inverser des bits dans les solutions candidates enfants créées. Généralement, le taux de mutation est défini sur 1/L, où L est la longueur de la chaîne de bits.
Chaque bit d'un chromosome à valeur binaire a généralement une faible probabilité d'être inversé. Pour un chromosome comportant m bits, ce taux de mutation est généralement fixé à 1/m, ce qui donne une moyenne d'une mutation par chromosome enfant.
— Page 155, Algorithmes d'optimisation, 2019.
Par exemple, si un problème utilisait une chaîne de bits de 20 bits, un bon taux de mutation par défaut serait (1/20)=0,05 ou une probabilité de 5 pour cent.
Ceci définit la procédure simple de l’algorithme génétique. Il s’agit d’un vaste domaine d’étude et il existe de nombreuses extensions à l’algorithme.
Maintenant que nous sommes familiers avec la procédure simple de l’algorithme génétique, voyons comment nous pourrions l’implémenter à partir de zéro.
Algorithme génétique à partir de zéro
Dans cette section, nous développerons une implémentation de l'algorithme génétique.
La première étape consiste à créer une population de chaînes de bits aléatoires. Nous pourrions utiliser des valeurs booléennes Vrai et False, des valeurs de chaîne « 0 » et « 1 », ou des valeurs entières 0 et 1. Dans ce cas, nous utiliserons des valeurs entières.
Nous pouvons générer un tableau de valeurs entières dans une plage à l'aide de la fonction randint(), et nous pouvons spécifier la plage sous forme de valeurs commençant à 0 et inférieures à 2, par ex. 0 ou 1. Nous représenterons également une solution candidate sous forme de liste au lieu d'un tableau NumPy pour garder les choses simples.
Une population initiale de chaînes de bits aléatoires peut être créée comme suit, où « n_pop » est un hyperparamètre qui contrôle la taille de la population et « n_bits » est un hyperparamètre qui définit le nombre de bits dans une solution candidate unique :
...
# initial population of random bitstring
pop = [randint(0, 2, n_bits).tolist() for _ in range(n_pop)]
Ensuite, nous pouvons énumérer sur un nombre fixe d'itérations d'algorithme, dans ce cas, contrôlées par un hyperparamètre nommé « n_iter ».
...
# enumerate generations
for gen in range(n_iter):
...
La première étape de l’itération de l’algorithme consiste à évaluer toutes les solutions candidates.
Nous utiliserons une fonction nommée objective() comme fonction objectif générique et l'appellerons pour obtenir un score de condition physique, que nous minimiserons.
...
# evaluate all candidates in the population
scores = [objective(c) for c in pop]
On peut alors sélectionner les parents qui serviront à créer des enfants.
La procédure de sélection du tournoi peut être implémentée comme une fonction qui prend la population et renvoie un parent sélectionné. La valeur k est fixée à 3 avec un argument par défaut, mais vous pouvez expérimenter différentes valeurs si vous le souhaitez.
# tournament selection
def selection(pop, scores, k=3):
# first random selection
selection_ix = randint(len(pop))
for ix in randint(0, len(pop), k-1):
# check if better (e.g. perform a tournament)
if scores[ix] < scores[selection_ix]:
selection_ix = ix
return pop[selection_ix]
On peut alors appeler cette fonction une fois pour chaque position dans la population afin de créer une liste de parents.
...
# select parents
selected = [selection(pop, scores) for _ in range(n_pop)]
Nous pourrons alors créer la prochaine génération.
Cela nécessite d’abord une fonction pour effectuer un croisement. Cette fonction prendra deux parents et le taux de croisement. Le taux de croisement est un hyperparamètre qui détermine si le croisement est effectué ou non, et sinon, les parents sont copiés dans la génération suivante. Il s'agit d'une probabilité et sa valeur est généralement élevée, proche de 1,0.
La fonction crossover() ci-dessous implémente le croisement en utilisant un tirage d'un nombre aléatoire dans la plage [0,1] pour déterminer si le croisement est effectué, puis en sélectionnant un point de partage valide si le croisement doit être effectué.
# crossover two parents to create two children
def crossover(p1, p2, r_cross):
# children are copies of parents by default
c1, c2 = p1.copy(), p2.copy()
# check for recombination
if rand() < r_cross:
# select crossover point that is not on the end of the string
pt = randint(1, len(p1)-2)
# perform crossover
c1 = p1[:pt] + p2[pt:]
c2 = p2[:pt] + p1[pt:]
return [c1, c2]
Nous avons également besoin d’une fonction pour effectuer une mutation.
Cette procédure retourne simplement les bits avec une faible probabilité contrôlée par l'hyperparamètre « r_mut ».
# mutation operator
def mutation(bitstring, r_mut):
for i in range(len(bitstring)):
# check for a mutation
if rand() < r_mut:
# flip the bit
bitstring[i] = 1 - bitstring[i]
Nous pouvons ensuite parcourir la liste des parents et créer une liste d'enfants à utiliser comme génération suivante, en appelant les fonctions de croisement et de mutation si nécessaire.
...
# create the next generation
children = list()
for i in range(0, n_pop, 2):
# get selected parents in pairs
p1, p2 = selected[i], selected[i+1]
# crossover and mutation
for c in crossover(p1, p2, r_cross):
# mutation
mutation(c, r_mut)
# store for next generation
children.append(c)
Nous pouvons lier tout cela ensemble dans une fonction nommée genetic_algorithm() qui prend le nom de la fonction objectif et les hyperparamètres de la recherche, et renvoie la meilleure solution trouvée lors de la recherche.
# genetic algorithm
def genetic_algorithm(objective, n_bits, n_iter, n_pop, r_cross, r_mut):
# initial population of random bitstring
pop = [randint(0, 2, n_bits).tolist() for _ in range(n_pop)]
# keep track of best solution
best, best_eval = 0, objective(pop[0])
# enumerate generations
for gen in range(n_iter):
# evaluate all candidates in the population
scores = [objective(c) for c in pop]
# check for new best solution
for i in range(n_pop):
if scores[i] < best_eval:
best, best_eval = pop[i], scores[i]
print(">%d, new best f(%s) = %.3f" % (gen, pop[i], scores[i]))
# select parents
selected = [selection(pop, scores) for _ in range(n_pop)]
# create the next generation
children = list()
for i in range(0, n_pop, 2):
# get selected parents in pairs
p1, p2 = selected[i], selected[i+1]
# crossover and mutation
for c in crossover(p1, p2, r_cross):
# mutation
mutation(c, r_mut)
# store for next generation
children.append(c)
# replace population
pop = children
return [best, best_eval]
Maintenant que nous avons développé une implémentation de l’algorithme génétique, explorons comment nous pourrions l’appliquer à une fonction objective.
Algorithme génétique pour OneMax
Dans cette section, nous appliquerons l'algorithme génétique à un problème d'optimisation basé sur des chaînes binaires.
Le problème s'appelle OneMax et évalue une chaîne binaire en fonction du nombre de 1 dans la chaîne. Par exemple, une chaîne de bits d’une longueur de 20 bits aura un score de 20 pour une chaîne composée uniquement de 1.
Étant donné que nous avons implémenté l'algorithme génétique pour minimiser la fonction objectif, nous pouvons ajouter un signe négatif à cette évaluation afin que les grandes valeurs positives deviennent de grandes valeurs négatives.
La fonction onemax() ci-dessous implémente cela et prend une chaîne de bits de valeurs entières en entrée et renvoie la somme négative des valeurs.
# objective function
def onemax(x):
return -sum(x)
Ensuite, nous pouvons configurer la recherche.
La recherche s'exécutera sur 100 itérations et nous utiliserons 20 bits dans nos solutions candidates, ce qui signifie que l'adéquation optimale sera de -20,0.
La taille de la population sera de 100 personnes et nous utiliserons un taux de croisement de 90 pour cent et un taux de mutation de 5 pour cent. Cette configuration a été choisie après quelques essais et erreurs.
...
# define the total iterations
n_iter = 100
# bits
n_bits = 20
# define the population size
n_pop = 100
# crossover rate
r_cross = 0.9
# mutation rate
r_mut = 1.0 / float(n_bits)
La recherche peut alors être lancée et le meilleur résultat signalé.
...
# perform the genetic algorithm search
best, score = genetic_algorithm(onemax, n_bits, n_iter, n_pop, r_cross, r_mut)
print('Done!')
print('f(%s) = %f' % (best, score))
En reliant cela ensemble, l'exemple complet d'application de l'algorithme génétique à la fonction objectif OneMax est répertorié ci-dessous.
# genetic algorithm search of the one max optimization problem
from numpy.random import randint
from numpy.random import rand
# objective function
def onemax(x):
return -sum(x)
# tournament selection
def selection(pop, scores, k=3):
# first random selection
selection_ix = randint(len(pop))
for ix in randint(0, len(pop), k-1):
# check if better (e.g. perform a tournament)
if scores[ix] < scores[selection_ix]:
selection_ix = ix
return pop[selection_ix]
# crossover two parents to create two children
def crossover(p1, p2, r_cross):
# children are copies of parents by default
c1, c2 = p1.copy(), p2.copy()
# check for recombination
if rand() < r_cross:
# select crossover point that is not on the end of the string
pt = randint(1, len(p1)-2)
# perform crossover
c1 = p1[:pt] + p2[pt:]
c2 = p2[:pt] + p1[pt:]
return [c1, c2]
# mutation operator
def mutation(bitstring, r_mut):
for i in range(len(bitstring)):
# check for a mutation
if rand() < r_mut:
# flip the bit
bitstring[i] = 1 - bitstring[i]
# genetic algorithm
def genetic_algorithm(objective, n_bits, n_iter, n_pop, r_cross, r_mut):
# initial population of random bitstring
pop = [randint(0, 2, n_bits).tolist() for _ in range(n_pop)]
# keep track of best solution
best, best_eval = 0, objective(pop[0])
# enumerate generations
for gen in range(n_iter):
# evaluate all candidates in the population
scores = [objective(c) for c in pop]
# check for new best solution
for i in range(n_pop):
if scores[i] < best_eval:
best, best_eval = pop[i], scores[i]
print(">%d, new best f(%s) = %.3f" % (gen, pop[i], scores[i]))
# select parents
selected = [selection(pop, scores) for _ in range(n_pop)]
# create the next generation
children = list()
for i in range(0, n_pop, 2):
# get selected parents in pairs
p1, p2 = selected[i], selected[i+1]
# crossover and mutation
for c in crossover(p1, p2, r_cross):
# mutation
mutation(c, r_mut)
# store for next generation
children.append(c)
# replace population
pop = children
return [best, best_eval]
# define the total iterations
n_iter = 100
# bits
n_bits = 20
# define the population size
n_pop = 100
# crossover rate
r_cross = 0.9
# mutation rate
r_mut = 1.0 / float(n_bits)
# perform the genetic algorithm search
best, score = genetic_algorithm(onemax, n_bits, n_iter, n_pop, r_cross, r_mut)
print('Done!')
print('f(%s) = %f' % (best, score))
L’exécution de l’exemple rapportera le meilleur résultat tel qu’il est trouvé en cours de route, puis la meilleure solution finale à la fin de la recherche, qui devrait être la solution optimale.
Remarque : Vos résultats peuvent varier en raison de la nature stochastique de l'algorithme ou de la procédure d'évaluation, ou des différences de précision numérique. Pensez à exécuter l’exemple plusieurs fois et comparez le résultat moyen.
Dans ce cas, nous pouvons voir que la recherche a trouvé la solution optimale après environ huit générations.
>0, new best f([1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1]) = -14.000
>0, new best f([1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0]) = -15.000
>1, new best f([1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1]) = -16.000
>2, new best f([0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1]) = -17.000
>2, new best f([1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) = -19.000
>8, new best f([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) = -20.000
Done!
f([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) = -20.000000
Algorithme génétique pour l'optimisation continue des fonctions
Optimiser la fonction OneMax n’est pas très intéressant ; nous sommes plus susceptibles de vouloir optimiser une fonction continue.
Par exemple, nous pouvons définir la fonction de minimisation x^2 qui prend des variables d'entrée et a un optimal à f(0, 0)=0,0.
# objective function
def objective(x):
return x[0]**2.0 + x[1]**2.0
Nous pouvons minimiser cette fonction avec un algorithme génétique.
Tout d’abord, nous devons définir les limites de chaque variable d’entrée.
...
# define range for input
bounds = [[-5.0, 5.0], [-5.0, 5.0]]
Nous prendrons l'hyperparamètre « n_bits » comme nombre de bits par variable d'entrée pour la fonction objectif et le fixerons à 16 bits.
...
# bits per variable
n_bits = 16
Cela signifie que notre chaîne de bits réelle aura (16 * 2)=32 bits, étant donné les deux variables d'entrée.
Nous devons mettre à jour notre taux de mutation en conséquence.
...
# mutation rate
r_mut = 1.0 / (float(n_bits) * len(bounds))
Ensuite, nous devons nous assurer que la population initiale crée des chaînes de bits aléatoires suffisamment grandes.
...
# initial population of random bitstring
pop = [randint(0, 2, n_bits*len(bounds)).tolist() for _ in range(n_pop)]
Enfin, nous devons décoder les chaînes de bits en nombres avant de les évaluer chacune avec la fonction objectif.
Nous pouvons y parvenir en décodant d’abord chaque sous-chaîne en un entier, puis en mettant l’entier à l’échelle souhaitée. Cela donnera un vecteur de valeurs dans la plage qui pourra ensuite être fourni à la fonction objectif pour évaluation.
La fonction decode() ci-dessous implémente cela, en prenant les limites de la fonction, le nombre de bits par variable et une chaîne de bits en entrée et renvoie une liste de valeurs réelles décodées.
# decode bitstring to numbers
def decode(bounds, n_bits, bitstring):
decoded = list()
largest = 2**n_bits
for i in range(len(bounds)):
# extract the substring
start, end = i * n_bits, (i * n_bits)+n_bits
substring = bitstring[start:end]
# convert bitstring to a string of chars
chars = ''.join([str(s) for s in substring])
# convert string to integer
integer = int(chars, 2)
# scale integer to desired range
value = bounds[i][0] + (integer/largest) * (bounds[i][1] - bounds[i][0])
# store
decoded.append(value)
return decoded
On peut alors appeler cela au début de la boucle algorithmique pour décoder la population, puis évaluer la version décodée de la population.
...
# decode population
decoded = [decode(bounds, n_bits, p) for p in pop]
# evaluate all candidates in the population
scores = [objective(d) for d in decoded]
En reliant cela ensemble, l’exemple complet de l’algorithme génétique pour l’optimisation continue des fonctions est répertorié ci-dessous.
# genetic algorithm search for continuous function optimization
from numpy.random import randint
from numpy.random import rand
# objective function
def objective(x):
return x[0]**2.0 + x[1]**2.0
# decode bitstring to numbers
def decode(bounds, n_bits, bitstring):
decoded = list()
largest = 2**n_bits
for i in range(len(bounds)):
# extract the substring
start, end = i * n_bits, (i * n_bits)+n_bits
substring = bitstring[start:end]
# convert bitstring to a string of chars
chars = ''.join([str(s) for s in substring])
# convert string to integer
integer = int(chars, 2)
# scale integer to desired range
value = bounds[i][0] + (integer/largest) * (bounds[i][1] - bounds[i][0])
# store
decoded.append(value)
return decoded
# tournament selection
def selection(pop, scores, k=3):
# first random selection
selection_ix = randint(len(pop))
for ix in randint(0, len(pop), k-1):
# check if better (e.g. perform a tournament)
if scores[ix] < scores[selection_ix]:
selection_ix = ix
return pop[selection_ix]
# crossover two parents to create two children
def crossover(p1, p2, r_cross):
# children are copies of parents by default
c1, c2 = p1.copy(), p2.copy()
# check for recombination
if rand() < r_cross:
# select crossover point that is not on the end of the string
pt = randint(1, len(p1)-2)
# perform crossover
c1 = p1[:pt] + p2[pt:]
c2 = p2[:pt] + p1[pt:]
return [c1, c2]
# mutation operator
def mutation(bitstring, r_mut):
for i in range(len(bitstring)):
# check for a mutation
if rand() < r_mut:
# flip the bit
bitstring[i] = 1 - bitstring[i]
# genetic algorithm
def genetic_algorithm(objective, bounds, n_bits, n_iter, n_pop, r_cross, r_mut):
# initial population of random bitstring
pop = [randint(0, 2, n_bits*len(bounds)).tolist() for _ in range(n_pop)]
# keep track of best solution
best, best_eval = 0, objective(decode(bounds, n_bits, pop[0]))
# enumerate generations
for gen in range(n_iter):
# decode population
decoded = [decode(bounds, n_bits, p) for p in pop]
# evaluate all candidates in the population
scores = [objective(d) for d in decoded]
# check for new best solution
for i in range(n_pop):
if scores[i] < best_eval:
best, best_eval = pop[i], scores[i]
print(">%d, new best f(%s) = %f" % (gen, decoded[i], scores[i]))
# select parents
selected = [selection(pop, scores) for _ in range(n_pop)]
# create the next generation
children = list()
for i in range(0, n_pop, 2):
# get selected parents in pairs
p1, p2 = selected[i], selected[i+1]
# crossover and mutation
for c in crossover(p1, p2, r_cross):
# mutation
mutation(c, r_mut)
# store for next generation
children.append(c)
# replace population
pop = children
return [best, best_eval]
# define range for input
bounds = [[-5.0, 5.0], [-5.0, 5.0]]
# define the total iterations
n_iter = 100
# bits per variable
n_bits = 16
# define the population size
n_pop = 100
# crossover rate
r_cross = 0.9
# mutation rate
r_mut = 1.0 / (float(n_bits) * len(bounds))
# perform the genetic algorithm search
best, score = genetic_algorithm(objective, bounds, n_bits, n_iter, n_pop, r_cross, r_mut)
print('Done!')
decoded = decode(bounds, n_bits, best)
print('f(%s) = %f' % (decoded, score))
L’exécution de l’exemple rapporte les meilleurs résultats décodés en cours de route et la meilleure solution décodée à la fin de l’exécution.
Remarque : Vos résultats peuvent varier en raison de la nature stochastique de l'algorithme ou de la procédure d'évaluation, ou des différences de précision numérique. Pensez à exécuter l’exemple plusieurs fois et comparez le résultat moyen.
Dans ce cas, on peut voir que l'algorithme découvre une entrée très proche de f(0.0, 0.0)=0.0.
>0, new best f([-0.785064697265625, -0.807647705078125]) = 1.268621
>0, new best f([0.385894775390625, 0.342864990234375]) = 0.266471
>1, new best f([-0.342559814453125, -0.1068115234375]) = 0.128756
>2, new best f([-0.038909912109375, 0.30242919921875]) = 0.092977
>2, new best f([0.145721435546875, 0.1849365234375]) = 0.055436
>3, new best f([0.14404296875, -0.029754638671875]) = 0.021634
>5, new best f([0.066680908203125, 0.096435546875]) = 0.013746
>5, new best f([-0.036468505859375, -0.10711669921875]) = 0.012804
>6, new best f([-0.038909912109375, -0.099639892578125]) = 0.011442
>7, new best f([-0.033111572265625, 0.09674072265625]) = 0.010455
>7, new best f([-0.036468505859375, 0.05584716796875]) = 0.004449
>10, new best f([0.058746337890625, 0.008087158203125]) = 0.003517
>10, new best f([-0.031585693359375, 0.008087158203125]) = 0.001063
>12, new best f([0.022125244140625, 0.008087158203125]) = 0.000555
>13, new best f([0.022125244140625, 0.00701904296875]) = 0.000539
>13, new best f([-0.013885498046875, 0.008087158203125]) = 0.000258
>16, new best f([-0.011444091796875, 0.00518798828125]) = 0.000158
>17, new best f([-0.0115966796875, 0.00091552734375]) = 0.000135
>17, new best f([-0.004730224609375, 0.00335693359375]) = 0.000034
>20, new best f([-0.004425048828125, 0.00274658203125]) = 0.000027
>21, new best f([-0.002288818359375, 0.00091552734375]) = 0.000006
>22, new best f([-0.001983642578125, 0.00091552734375]) = 0.000005
>22, new best f([-0.001983642578125, 0.0006103515625]) = 0.000004
>24, new best f([-0.001373291015625, 0.001068115234375]) = 0.000003
>25, new best f([-0.001373291015625, 0.00091552734375]) = 0.000003
>26, new best f([-0.001373291015625, 0.0006103515625]) = 0.000002
>27, new best f([-0.001068115234375, 0.0006103515625]) = 0.000002
>29, new best f([-0.000152587890625, 0.00091552734375]) = 0.000001
>33, new best f([-0.0006103515625, 0.0]) = 0.000000
>34, new best f([-0.000152587890625, 0.00030517578125]) = 0.000000
>43, new best f([-0.00030517578125, 0.0]) = 0.000000
>60, new best f([-0.000152587890625, 0.000152587890625]) = 0.000000
>65, new best f([-0.000152587890625, 0.0]) = 0.000000
Done!
f([-0.000152587890625, 0.0]) = 0.000000
Lectures complémentaires
Cette section fournit plus de ressources sur le sujet si vous souhaitez approfondir.
Livres
- Algorithmes génétiques dans la recherche, l'optimisation et l'apprentissage automatique, 1989.
- Une introduction aux algorithmes génétiques, 1998.
- Algorithmes d'optimisation, 2019.
- Essentiels de la métaheuristique, 2011.
- Intelligence computationnelle : une introduction, 2007.
API
- API numpy.random.randint.
Articles
- Algorithme génétique, Wikipédia.
- Algorithmes génétiques, Scholarpedia.
Résumé
Dans ce tutoriel, vous avez découvert l'optimisation des algorithmes génétiques.
Concrètement, vous avez appris :
- L'algorithme génétique est un algorithme d'optimisation stochastique inspiré de l'évolution.
- Comment implémenter l'algorithme génétique à partir de zéro en Python.
- Comment appliquer l'algorithme génétique à une fonction objectif continue.
Avez-vous des questions ?
Posez vos questions dans les commentaires ci-dessous et je ferai de mon mieux pour y répondre.