Cet article décrit la bibliothÚque
optlib , conçue pour résoudre les problÚmes d'optimisation globale dans le langage Rust. Au moment d'écrire ces lignes, cette bibliothÚque implémente un algorithme génétique pour trouver le minimum global d'une fonction. La bibliothÚque optlib n'est pas liée à un type d'entrée spécifique pour une fonction optimisée. De plus, la bibliothÚque est construite de telle maniÚre que lors de l'utilisation de l'algorithme génétique, il est facile de changer les algorithmes de croisement, de mutation, de sélection et d'autres étapes de l'algorithme génétique. En fait, l'algorithme génétique est assemblé comme s'il s'agissait de cubes.
Le problĂšme de l'optimisation globale
Le problÚme d'optimisation est généralement formulé comme suit.
Pour une fonction donnée f ( x ), parmi toutes les valeurs possibles de x, trouver une valeur x 'telle que f (x') prenne une valeur minimale (ou maximale). De plus, x peut appartenir à différents ensembles - nombres naturels, nombres réels, nombres complexes, vecteurs ou matrices.
Par l'extrémum de la fonction f ( x ), nous entendons un point x ' auquel la dérivée de la fonction f ( x ) est égale à zéro.
Il existe de nombreux algorithmes garantissant de trouver l'extremum de la fonction, qui est un minimum ou un maximum local prÚs d'un point de départ donné x 0 . De tels algorithmes comprennent, par exemple, des algorithmes de descente de gradient. Cependant, dans la pratique, il est souvent nécessaire de trouver le minimum (ou maximum) global d'une fonction qui, dans une gamme donnée de x, en plus d'un extremum global, a de nombreux extrema locaux.
Les algorithmes de gradient ne peuvent pas faire face à l'optimisation d'une telle fonction, car leur solution converge vers l'extremum le plus proche prÚs du point de départ. Pour les problÚmes de recherche d'un maximum ou d'un minimum global, les algorithmes dits d'optimisation globale sont utilisés. Ces algorithmes ne garantissent pas de trouver un extremum global, car sont des algorithmes probabilistes, mais il ne reste plus qu'à essayer différents algorithmes pour une tùche spécifique et voir quel algorithme sera le mieux en mesure d'optimiser, de préférence dans les plus brefs délais et avec la probabilité maximale de trouver un extremum global.
L'un des algorithmes les plus connus (et difficiles Ă mettre en Ćuvre) est l'algorithme gĂ©nĂ©tique.
Le schéma général de l'algorithme génétique
L'idée d'un algorithme génétique est née progressivement et s'est formée à la fin des années 1960 - début des années 1970. Les algorithmes génétiques ont connu un développement puissant aprÚs la sortie du livre de John Holland «Adaptation dans les systÚmes naturels et artificiels» (1975)
L'algorithme génétique est basé sur la modélisation d'une population d'individus sur un grand nombre de générations. Dans le processus de l'algorithme génétique, de nouveaux individus avec les meilleurs paramÚtres apparaissent et les individus les moins performants meurent. Pour plus de précision, les termes suivants sont utilisés dans l'algorithme génétique.
- Un individu est une valeur de x parmi l'ensemble des valeurs possibles avec la valeur de la fonction objectif pour un point x donné.
- Chromosomes - la valeur de x .
- Chromosome - la valeur de x i si x est un vecteur.
- La fonction fitness (fonction fitness, fonction objectif) est la fonction optimisée f ( x ).
- Une population est un ensemble d'individus.
- Génération - le nombre d'itérations de l'algorithme génétique.
Chaque individu représente une valeur de
x parmi l'ensemble de toutes les solutions possibles. La valeur de la fonction optimisée (à l'avenir, par souci de concision, nous supposons que nous recherchons le minimum de la fonction) est calculée pour chaque valeur de
x . Nous supposons que moins la fonction objectif est importante, meilleure est cette solution.
Les algorithmes gĂ©nĂ©tiques sont utilisĂ©s dans de nombreux domaines. Par exemple, ils peuvent ĂȘtre utilisĂ©s pour sĂ©lectionner des poids dans les rĂ©seaux de neurones. De nombreux systĂšmes de CAO utilisent un algorithme gĂ©nĂ©tique pour optimiser les paramĂštres de l'appareil afin de rĂ©pondre aux exigences spĂ©cifiĂ©es. De plus, des algorithmes d'optimisation globale peuvent ĂȘtre utilisĂ©s pour sĂ©lectionner l'emplacement des conducteurs et des Ă©lĂ©ments sur la carte.
Le diagramme structurel de l'algorithme génétique est illustré dans la figure suivante:

Nous considérons chaque étape de l'algorithme plus en détail.
Créer une population initiale
La premiÚre étape de l'algorithme est la création de la population initiale, c'est-à -dire la création de nombreux individus avec différentes valeurs de chromosomes x . En rÚgle générale, la population initiale est créée à partir d'individus ayant une valeur chromosomique aléatoire, tout en essayant de garantir que les valeurs chromosomiques de la population couvrent la zone de recherche de la solution de maniÚre relativement uniforme, à moins qu'il n'y ait des hypothÚses quant à l'emplacement de l'extremum global.
Au lieu de distribuer aléatoirement des chromosomes, vous pouvez créer des chromosomes afin que les valeurs initiales de x soient uniformément réparties dans la zone de recherche avec une étape donnée, qui dépend du nombre d'individus qui seront créés à ce stade de l'algorithme.
Plus il y aura d'individus créés à ce stade, plus il est probable que l'algorithme trouvera la bonne solution, et à mesure que la taille de la population initiale augmente, en rÚgle générale, moins d'itérations de l'algorithme génétique (nombre de générations) sont nécessaires. Cependant, avec l'augmentation de la taille de la population, un nombre croissant de calculs de la fonction objective et la performance d'autres opérations génétiques aux étapes suivantes de l'algorithme sont nécessaires. Autrement dit, avec une augmentation de la taille de la population, le temps de calcul d'une génération augmente.
En principe, la taille de la population ne doit pas rester constante tout au long du travail de l'algorithme gĂ©nĂ©tique, souvent lorsque le nombre de gĂ©nĂ©ration augmente, la taille de la population peut ĂȘtre rĂ©duite, car au fil du temps, un nombre croissant d'individus seront plus proches de la solution souhaitĂ©e. Cependant, la taille de la population est souvent maintenue Ă peu prĂšs constante.
Sélection des individus pour le croisement
AprÚs avoir créé une population, il est nécessaire de déterminer quels individus participeront à l'opération de croisement (voir le paragraphe suivant). Il existe différents algorithmes pour cette étape. Le plus simple d'entre eux est de croiser chaque individu avec chacun, mais à l'étape suivante, vous devrez effectuer trop d'opérations de croisement et calculer les valeurs de la fonction objectif.
Il est préférable de donner une plus grande chance d'accoupler des individus avec des chromosomes plus performants (avec une valeur minimale de la fonction objective), et les individus dont la fonction objective est plus (pire) de priver la capacité de laisser la progéniture.
Ainsi, à ce stade, vous devez créer des paires d'individus (ou pas nécessairement des paires si plus d'individus peuvent participer à la traversée), pour lesquels l'opération de traversée sera effectuée à l'étape suivante.
Ici, vous pouvez également appliquer différents algorithmes. Par exemple, créez des paires au hasard. Ou vous pouvez trier les individus par ordre croissant en fonction de la valeur de la fonction objectif et créer des paires d'individus situées plus prÚs du début de la liste triée (par exemple, à partir d'individus dans la premiÚre moitié de la liste, dans le premier tiers de la liste, etc.) Cette méthode est mauvaise en ce qu'elle prend du temps pour trier les individus.
La mĂ©thode du tournoi est souvent utilisĂ©e. Lorsque plusieurs individus sont sĂ©lectionnĂ©s au hasard pour le rĂŽle de chaque candidat au croisement, parmi lesquels l'individu ayant la meilleure valeur de la fonction objectif est envoyĂ© au futur couple. Et mĂȘme ici, on peut introduire un Ă©lĂ©ment de hasard, donnant une petite chance Ă l'individu ayant la pire valeur de la fonction objectif de "vaincre" l'individu avec la meilleure valeur de la fonction objectif. Cela vous permet de maintenir une population plus hĂ©tĂ©rogĂšne, la protĂ©geant de la dĂ©gĂ©nĂ©rescence, c'est-Ă -dire d'une situation oĂč tous les individus ont des valeurs chromosomiques approximativement Ă©gales, ce qui Ă©quivaut Ă bloquer l'algorithme Ă un extrĂȘme, peut-ĂȘtre pas global.
Le résultat de cette opération sera une liste de partenaires à traverser.
Croisement
Le croisement est une opĂ©ration gĂ©nĂ©tique qui crĂ©e de nouveaux individus avec de nouvelles valeurs chromosomiques. De nouveaux chromosomes sont créés sur la base des chromosomes des parents. Le plus souvent, lors du croisement d'un ensemble de partenaires, une fille est créée, mais thĂ©oriquement, plus d'enfants peuvent ĂȘtre créés.
L'algorithme de croisement peut Ă©galement ĂȘtre implĂ©mentĂ© de diffĂ©rentes maniĂšres. Si le type de chromosome est un nombre chez les individus, alors la chose la plus simple qui puisse ĂȘtre faite est de crĂ©er un nouveau chromosome comme moyenne arithmĂ©tique ou moyenne gĂ©omĂ©trique des chromosomes des parents. Pour de nombreuses tĂąches, cela suffit, mais le plus souvent, d'autres algorithmes basĂ©s sur des opĂ©rations binaires sont utilisĂ©s.
Les croisements au niveau du bit fonctionnent de telle maniĂšre que le chromosome fille se compose d'une partie des bits d'un parent et d'une partie des bits d'un autre parent, comme le montre la figure suivante:
Le point de partage parent est généralement sélectionné au hasard. Il n'est pas nécessaire de créer deux enfants avec une telle croix, souvent limitée à l'un d'eux.
Si le point de partage des chromosomes parents est un, un tel croisement est appelé point unique. Mais vous pouvez également utiliser le fractionnement multipoint, lorsque les chromosomes parents sont divisés en plusieurs sections, comme le montre la figure suivante:
Dans ce cas, plusieurs combinaisons de chromosomes filles sont possibles.
Si les chromosomes sont des nombres Ă virgule flottante, alors toutes les mĂ©thodes dĂ©crites ci-dessus peuvent leur ĂȘtre appliquĂ©es, mais elles ne seront pas trĂšs efficaces. Par exemple, si les nombres Ă virgule flottante sont croisĂ©s au niveau du bit, comme dĂ©crit prĂ©cĂ©demment, de nombreuses opĂ©rations de croisement crĂ©eront des chromosomes filles qui ne diffĂ©reront de l'un des parents qu'Ă la dĂ©cimale Ă©loignĂ©e. La situation est particuliĂšrement aggravĂ©e lors de l'utilisation de nombres Ă virgule flottante double prĂ©cision.
Pour résoudre ce problÚme, il est nécessaire de rappeler que les nombres à virgule flottante selon la norme IEEE 754 sont stockés sous forme de trois nombres entiers: S (un bit), M et N, à partir desquels le nombre à virgule flottante est calculé comme x = (-1) S à M à 2 N. Une façon de croiser des nombres à virgule flottante consiste à diviser d'abord le nombre en composantes S, M, N, qui sont des nombres entiers, puis à appliquer les opérations de croisement au niveau du bit décrites ci-dessus aux nombres M et N, sélectionner le signe S de l'un des les parents, et à partir des valeurs obtenues pour faire un chromosome fille.
Dans de nombreux problĂšmes, un individu n'a pas un, mais plusieurs chromosomes, et il peut mĂȘme ĂȘtre de diffĂ©rents types (nombres entiers et nombres Ă virgule flottante). Ensuite, il existe encore plus d'options pour traverser de tels chromosomes. Par exemple, lors de la crĂ©ation d'un individu fille, vous pouvez traverser tous les chromosomes des parents, ou vous pouvez transfĂ©rer complĂštement certains chromosomes de l'un des parents sans changements.
Mutation
La mutation est une Ă©tape importante de l'algorithme gĂ©nĂ©tique qui prend en charge la diversitĂ© des chromosomes des individus et rĂ©duit ainsi les chances que la solution converge rapidement vers un minimum local au lieu d'un minimum global. Une mutation est un changement alĂ©atoire du chromosome d'un individu qui vient d'ĂȘtre créé par croisement.
En rÚgle générale, la probabilité d'une mutation n'est pas réglée trÚs haut afin que la mutation n'interfÚre pas avec la convergence de l'algorithme, sinon elle gùtera les individus avec des chromosomes réussis.
En plus du croisement, diffĂ©rents algorithmes peuvent ĂȘtre utilisĂ©s pour la mutation. Par exemple, vous pouvez remplacer un chromosome ou plusieurs chromosomes par une valeur alĂ©atoire. Mais la mutation au niveau du bit est le plus souvent utilisĂ©e lorsqu'un ou plusieurs bits sont inversĂ©s dans le chromosome (ou dans plusieurs chromosomes), comme le montrent les figures suivantes.
Mutation d'un bit:
Mutation de plusieurs bits:
Le nombre de bits d'une mutation peut ĂȘtre prĂ©dĂ©fini ou ĂȘtre une variable alĂ©atoire.
à la suite de mutations, les individus peuvent se révéler à la fois plus réussis et moins réussis, mais cette opération permet un chromosome réussi avec des ensembles de zéros et ceux que les individus parentaux n'ont pas dû apparaßtre avec une probabilité non nulle.
Sélection
à la suite du croisement et de la mutation subséquente, de nouveaux individus apparaissent. S'il n'y avait pas d'étape de sélection, le nombre d'individus augmenterait de façon exponentielle, ce qui nécessiterait de plus en plus de RAM et de temps de traitement pour chaque nouvelle génération de la population. Par conséquent, aprÚs l'apparition de nouveaux individus, il est nécessaire d'éliminer la population des individus les moins performants. C'est ce qui se passe au stade de la sélection.
Les algorithmes de sĂ©lection peuvent Ă©galement ĂȘtre diffĂ©rents. Souvent, les individus dont les chromosomes ne tombent pas dans un intervalle donnĂ© d'une recherche de solution sont d'abord rejetĂ©s.
Vous pouvez ensuite supprimer autant d'individus les moins performants que possible afin de maintenir une taille de population constante. Divers critĂšres probabilistes de sĂ©lection peuvent Ă©galement ĂȘtre appliquĂ©s, par exemple, la probabilitĂ© de sĂ©lection d'un individu peut dĂ©pendre de la valeur de la fonction objectif.
Conditions de fin d'algorithme
Comme dans les autres étapes de l'algorithme génétique, il existe plusieurs critÚres pour mettre fin à l'algorithme.
Le critĂšre le plus simple pour terminer un algorithme est d'interrompre l'algorithme aprĂšs un nombre d'itĂ©rations (gĂ©nĂ©ration) donnĂ©. Mais ce critĂšre doit ĂȘtre utilisĂ© avec prĂ©caution, car l'algorithme gĂ©nĂ©tique est probabiliste, et diffĂ©rents dĂ©marrages de l'algorithme peuvent converger Ă diffĂ©rentes vitesses. Habituellement, le critĂšre de terminaison par le numĂ©ro d'itĂ©ration est utilisĂ© comme critĂšre supplĂ©mentaire au cas oĂč l'algorithme ne trouverait pas de solution pendant un grand nombre d'itĂ©rations. Par consĂ©quent, un nombre suffisamment grand doit ĂȘtre dĂ©fini comme seuil du nombre d'itĂ©rations.
Un autre critĂšre d'arrĂȘt est que l'algorithme est interrompu si aucune nouvelle meilleure solution n'apparaĂźt pour un nombre donnĂ© d'itĂ©rations. Cela signifie que l'algorithme a trouvĂ© un extremum global ou est coincĂ© dans un extremum local.
Il existe Ă©galement une situation dĂ©favorable lorsque les chromosomes de tous les individus ont la mĂȘme signification. Il s'agit de la soi-disant population dĂ©gĂ©nĂ©rĂ©e. TrĂšs probablement dans ce cas, l'algorithme est coincĂ© dans un extrĂȘme, et pas nĂ©cessairement global. Seule une mutation rĂ©ussie est capable de sortir une population de cet Ă©tat, mais comme la probabilitĂ© d'une mutation est gĂ©nĂ©ralement Ă©tablie par une petite mutation, et qu'il est loin du fait que la mutation crĂ©era un individu plus rĂ©ussi, la situation avec une population dĂ©gĂ©nĂ©rĂ©e est gĂ©nĂ©ralement considĂ©rĂ©e comme une raison d'arrĂȘter l'algorithme gĂ©nĂ©tique. Pour vĂ©rifier ce critĂšre, il est nĂ©cessaire de comparer les chromosomes de tous les individus, s'il existe des diffĂ©rences entre eux, et s'il n'y a pas de diffĂ©rences, puis arrĂȘter l'algorithme.
Dans certains problĂšmes, il n'est pas nĂ©cessaire de trouver un minimum global, mais plutĂŽt de trouver une bonne solution - une solution pour laquelle la valeur de la fonction objectif est infĂ©rieure Ă une valeur donnĂ©e. Dans ce cas, l'algorithme peut ĂȘtre arrĂȘtĂ© Ă l'avance si la solution trouvĂ©e Ă la prochaine itĂ©ration satisfait ce critĂšre.
optlib
Optlib est une bibliothÚque pour le langage Rust, conçue pour optimiser les fonctions. Au moment d'écrire ces lignes, seul l'algorithme génétique est implémenté dans la bibliothÚque, mais à l'avenir, il est prévu d'étendre cette bibliothÚque en y ajoutant de nouveaux algorithmes d'optimisation. Optlib est entiÚrement écrit en rouille.
Optlib est une bibliothÚque open source distribuée sous licence MIT.
optlib :: génétique
De la description de l'algorithme gĂ©nĂ©tique, on peut voir que de nombreuses Ă©tapes de l'algorithme peuvent ĂȘtre implĂ©mentĂ©es de diffĂ©rentes maniĂšres, en les sĂ©lectionnant de sorte que pour une fonction objective donnĂ©e, l'algorithme converge vers la bonne solution en un minimum de temps.
Le module optlib :: génétique est conçu de maniÚre à pouvoir assembler l'algorithme génétique «à partir de cubes». Lors de la création d'une instance de la structure au sein de laquelle le travail de l'algorithme génétique aura lieu, vous devez spécifier les algorithmes qui seront utilisés pour créer la population, sélectionner les partenaires, les croisements, les mutations, la sélection et le critÚre utilisé pour interrompre l'algorithme.
La bibliothÚque possÚde déjà les algorithmes les plus fréquemment utilisés pour les étapes de l'algorithme génétique, mais vous pouvez créer vos propres types qui implémentent les algorithmes correspondants.
Dans la bibliothĂšque, le cas oĂč les chromosomes sont un vecteur de nombres fractionnaires, c'est-Ă -dire lorsque la fonction f ( x ) est minimisĂ©e, oĂč x est le vecteur des nombres Ă virgule flottante ( f32 ou f64 ).
Exemple d'optimisation utilisant optlib :: génétique
Avant de commencer à décrire le module génétique en détail, considérons un exemple de son utilisation pour minimiser la fonction de Schwefel. Cette fonction multidimensionnelle est calculée comme suit:
La fonction de Schweffel minimale dans l'intervalle -500 <= x i <= 500 est situĂ©e au point x ' , oĂč tous les x i = 420,9687 pour i = 1, 2, ..., N, et la valeur de la fonction Ă ce point est f ( x' ) = 0.
Si N = 2, alors l'image tridimensionnelle de cette fonction est la suivante:

Les extrĂȘmes sont plus clairement visibles si nous construisons les lignes de niveau de cette fonction:

L'exemple suivant montre comment utiliser le module génétique pour trouver la fonction de Schweffel minimale. Vous pouvez trouver cet exemple dans le code source dans le dossier examples / génétique-schwefel.
Cet exemple peut ĂȘtre exĂ©cutĂ© Ă partir de la racine source en exĂ©cutant la commande
cargo run --bin genetic-schwefel --release
Le résultat devrait ressembler à ceci:
Solution: 420.962615966796875 420.940093994140625 420.995391845703125 420.968505859375000 420.950866699218750 421.003784179687500 421.001281738281250 421.300537109375000 421.001708984375000 421.012603759765625 420.880493164062500 420.925079345703125 420.967559814453125 420.999237060546875 421.011505126953125 Goal: 0.015625000000000 Iterations count: 3000 Time elapsed: 2617 ms
La majeure partie de l'exemple de code est impliquĂ©e dans la crĂ©ation d'objets de trait pour les diffĂ©rentes Ă©tapes de l'algorithme gĂ©nĂ©tique. Comme cela a Ă©tĂ© Ă©crit au dĂ©but de l'article, chaque Ă©tape de l'algorithme gĂ©nĂ©tique peut ĂȘtre implĂ©mentĂ©e de diffĂ©rentes maniĂšres. Pour utiliser le module optlib :: gĂ©nĂ©tique, vous devez crĂ©er des objets trait qui implĂ©mentent chaque Ă©tape d'une maniĂšre ou d'une autre. Tous ces objets sont ensuite transfĂ©rĂ©s au constructeur de la structure GeneticOptimizer, au sein de laquelle l'algorithme gĂ©nĂ©tique est implĂ©mentĂ©.
La méthode find_min () de la structure GeneticOptimizer démarre l'exécution de l'algorithme génétique.
Types et objets de base
Caractéristiques de base du module optlib
La bibliothĂšque optlib a Ă©tĂ© dĂ©veloppĂ©e dans le but que de nouveaux algorithmes d'optimisation soient ajoutĂ©s Ă l'avenir afin que le programme puisse facilement passer d'un algorithme d'optimisation Ă un autre.Par consĂ©quent, le module optlib contient le trait Optimizer , qui comprend une seule mĂ©thode - find_min (), qui exĂ©cute l'algorithme d'optimisation lors de l'exĂ©cution. Ici, le type T est le type de l'objet, qui est un point dans l'espace de recherche de solution. Par exemple, dans l'exemple ci-dessus, il s'agit de Vec <Gene> (oĂč Gene est un alias pour f32). C'est-Ă -dire que c'est le type dont l'objet est alimentĂ© Ă l'entrĂ©e de la fonction objectif.
Le trait Optimizer est déclaré dans le module optlib comme suit:
pub trait Optimizer<T> { fn find_min(&mut self) -> Option<(&T, f64)>; }
La mĂ©thode find_min () de l'optim_ trait doit renvoyer un objet de type Option <(& T, f64)>, oĂč & T dans le tuple renvoyĂ© est la solution trouvĂ©e, et f64 est la valeur de la fonction objectif pour la solution trouvĂ©e. Si l'algorithme n'a pas pu trouver de solution, la fonction find_min () doit retourner None.
Ătant donnĂ© que de nombreux algorithmes d'optimisation stochastique utilisent des agents dits (en termes d'algorithme gĂ©nĂ©tique, un agent est un individu), le module optlib contient Ă©galement le trait AlgorithmWithAgents avec une seule mĂ©thode get_agents () qui devrait renvoyer le vecteur d'agent.
Un agent, à son tour, est une structure qui implémente un autre trait de optlib - Agent .
Les traits AlgorithmWithAgents et Agent sont déclarés dans le module optlib comme suit:
pub trait AlgorithmWithAgents<T> { type Agent: Agent<T>; fn get_agents(&self) -> Vec<&Self::Agent>; } pub trait Agent<T> { fn get_parameter(&self) -> &T; fn get_goal(&self) -> f64; }
Pour AlgorithmWithAgents et Agent, le type T a la mĂȘme signification que pour l'Optimizer, c'est-Ă -dire qu'il s'agit du type d'objet pour lequel la valeur de la fonction objectif est calculĂ©e.
Structure GeneticOptimizer - Implémentation d'un algorithme génétique
Les deux types sont implémentés pour la structure GeneticOptimizer - Optimizer et AlgorithmWithAgents.
Chaque Ă©tape de l'algorithme gĂ©nĂ©tique est reprĂ©sentĂ©e par son propre type, qui peut ĂȘtre implĂ©mentĂ© Ă partir de zĂ©ro ou utiliser l'une des implĂ©mentations optlib :: gĂ©nĂ©tique disponibles Ă l'intĂ©rieur. Pour chaque Ă©tape de l'algorithme gĂ©nĂ©tique, Ă l'intĂ©rieur du module optlib :: gĂ©nĂ©tique se trouve un sous-module avec des structures et fonctions auxiliaires qui mettent en Ćuvre les algorithmes les plus frĂ©quemment utilisĂ©s des Ă©tapes de l'algorithme gĂ©nĂ©tique. Ă propos de ces modules seront discutĂ©s ci-dessous. Ă l'intĂ©rieur de certains de ces sous-modules se trouve Ă©galement un sous-module vec_float qui implĂ©mente les Ă©tapes de l'algorithme dans le cas oĂč la fonction objectif est calculĂ©e Ă partir d'un vecteur de nombres Ă virgule flottante (f32 ou f64).
Le constructeur de la structure GeneticOptimizer est déclaré comme suit:
impl<T: Clone> GeneticOptimizer<T> { pub fn new( goal: Box<dyn Goal<T>>, stop_checker: Box<dyn StopChecker<T>>, creator: Box<dyn Creator<T>>, pairing: Box<dyn Pairing<T>>, cross: Box<dyn Cross<T>>, mutation: Box<dyn Mutation<T>>, selections: Vec<Box<dyn Selection<T>>>, pre_births: Vec<Box<dyn PreBirth<T>>>, loggers: Vec<Box<dyn Logger<T>>>, ) -> GeneticOptimizer<T> { ... } ... }
Le constructeur de GeneticOptimizer accepte de nombreux types d'objets qui affectent chaque étape de l'algorithme génétique, ainsi que la sortie des résultats:
- objectif: Box <dyn Goal <T>> - fonction objectif;
- stop_checker: Case <dyn StopChecker <T>> - critĂšre d'arrĂȘt;
- creator: Box <dyn Creator <T>> - crée la population initiale;
- appariement: Box <dyn Pairing <T>> - algorithme de sélection des partenaires pour le croisement;
- cross: Box <dyn Cross <T>> - algorithme de croisement;
- mutation: Box <dyn Mutation <T>> - algorithme de mutation;
- sélections: Vec <Box <dyn Selection <T> >> - un vecteur d'algorithmes de sélection;
- pre_births: Vec <Box <dyn PreBirth <T> >> - un vecteur d'algorithmes pour détruire les chromosomes infructueux avant de créer des individus;
- enregistreurs: Vec <Box <dyn Logger <T> >> - un vecteur d'objets qui préservent le journal de l'algorithme génétique.
Pour exĂ©cuter l'algorithme, vous devez exĂ©cuter la mĂ©thode find_min () du trait Optimizer. De plus, la structure GeneticOptimizer possĂšde la mĂ©thode next_iterations (), qui peut ĂȘtre utilisĂ©e pour continuer le calcul une fois la mĂ©thode find_min () terminĂ©e.
Parfois, aprĂšs l'arrĂȘt d'un algorithme, il est utile de modifier les paramĂštres de l'algorithme ou les mĂ©thodes utilisĂ©es. Cela peut ĂȘtre fait en utilisant les mĂ©thodes suivantes de la structure GeneticOptimizer: replace_pairing (), replace_cross (), replace_mutation (), replace_pre_birth (), replace_selection (), replace_stop_checker (). Ces mĂ©thodes vous permettent de remplacer les objets trait transmis Ă la structure GeneticOptimizer lors de sa crĂ©ation.
Les principaux types d'algorithme génétique sont discutés dans les sections suivantes.
Caractéristique de but - fonction objective
Le trait Goal est déclaré dans le module optlib :: génétique comme suit:
pub trait Goal<T> { fn get(&self, chromosomes: &T) -> f64; }
La méthode get () doit renvoyer la valeur de la fonction objectif pour le chromosome donné.
à l'intérieur du module optlib :: génétique :: objectif , il y a une structure GoalFromFunction qui implémente le trait Goal. Pour cette structure, il existe un constructeur qui prend une fonction, qui est la fonction cible. Autrement dit, en utilisant cette structure, vous pouvez créer un objet de type Objectif à partir d'une fonction réguliÚre.
CaractÚre créateur - création d'une population initiale
Le trait Créateur décrit le «créateur» de la population initiale. Ce trait est déclaré comme suit:
pub trait Creator<T: Clone> { fn create(&mut self) -> Vec<T>; }
La méthode create () doit renvoyer le vecteur des chromosomes sur la base duquel la population initiale sera créée.
Dans le cas oĂč une fonction de plusieurs nombres Ă virgule flottante est optimisĂ©e, le module optlib :: gĂ©nĂ©tique :: crĂ©ateur :: vec_float a une structure RandomCreator <G> pour crĂ©er une distribution initiale de chromosomes de maniĂšre alĂ©atoire.
Ci-aprÚs, le type <G> est le type d'un chromosome dans le vecteur chromosomique (parfois le terme "gÚne" est utilisé à la place du terme "chromosome"). Fondamentalement, le type <G> signifiera le type f32 ou f64 si les chromosomes sont du type Vec <f32> ou Vec <f64>.
La structure de RandomCreator <G> est déclarée comme suit:
pub struct RandomCreator<G: Clone + NumCast + PartialOrd> { ... }
Ici, NumCast est un type de la caisse numérique. Ce type vous permet de créer un type à partir d'autres types numériques à l'aide de la méthode from ().
Pour créer une structure RandomCreator <G>, vous devez utiliser la fonction new (), qui est déclarée comme suit:
pub fn new(population_size: usize, intervals: Vec<(G, G)>) -> RandomCreator<G> { ... }
Ici, population_size est la taille de la population (le nombre d'ensembles de chromosomes à créer), et les intervalles est le vecteur de tuples de deux éléments - la valeur minimale et maximale du chromosome (gÚne) correspondant dans l'ensemble de chromosomes, et la taille de ce vecteur détermine combien de chromosomes sont contenus dans l'ensemble de chromosomes chaque individu.
Dans l'exemple ci-dessus, la fonction Schweffel a été optimisée pour 15 variables de type f32. Chaque variable doit se situer dans la plage [-500; 500]. Autrement dit, pour créer une population, le vecteur d'intervalle doit contenir 15 tuples (-500.0f32, 500.0f32).
Appariement de types - sélection des partenaires à traverser
Le trait d'appariement est utilisé pour sélectionner les individus qui se métisseront. Ce trait est déclaré comme suit:
pub trait Pairing<T: Clone> { fn get_pairs(&mut self, population: &Population<T>) -> Vec<Vec<usize>>; }
La méthode get_pairs () prend un pointeur sur la structure de la population , qui contient tous les individus de la population, et également à travers cette structure, vous pouvez trouver les meilleurs et les pires individus de la population.
La méthode d'appariement get_pairs () doit renvoyer un vecteur qui contient des vecteurs qui stockent les indices des individus qui se métisseront. Selon l'algorithme de croisement (qui sera discuté dans la section suivante), deux individus ou plus peuvent se croiser.
Par exemple, si des individus avec l'index 0 et 10, 5 et 2, 6 et 3 sont croisés, la méthode get_pairs () doit renvoyer le vecteur vec! [Vec! [0, 10], vec! [5, 2], vec! [ 6, 3]].
Le module optlib :: génétique :: appariement contient deux structures qui implémentent divers algorithmes de sélection de partenaires.
Pour la structure RandomPairing , le type Pairing est implémenté de maniÚre à ce que les partenaires soient sélectionnés au hasard pour le croisement.
Pour la structure Tournament , le trait Pairing est implĂ©mentĂ© de telle maniĂšre que la mĂ©thode du tournoi est utilisĂ©e. La mĂ©thode du tournoi implique que chaque individu qui participera au cross doit vaincre un autre individu (la valeur de la fonction objective de l'individu gagnant doit ĂȘtre moindre). Si la mĂ©thode du tournoi utilise un tour, cela signifie que deux individus sont sĂ©lectionnĂ©s au hasard, et qu'un individu avec une valeur plus faible de la fonction objective parmi ces deux individus entre dans la future «famille». Si plusieurs tours sont utilisĂ©s, l'individu gagnant de cette maniĂšre devrait gagner contre plusieurs individus alĂ©atoires.
Le constructeur de la structure du tournoi est déclaré comme suit:
pub fn new(partners_count: usize, families_count: usize, rounds_count: usize) -> Tournament { ... }
Ici:
- partners_count - le nombre d'individus requis pour la traversée;
- families_count - le nombre de "familles", c'est-à -dire ensembles d'individus qui produiront une progéniture;
- rounds_count - le nombre de tours dans le tournoi.
Comme autre modification de la méthode du tournoi, vous pouvez utiliser le générateur de nombres aléatoires pour donner une petite chance de vaincre les individus avec la pire valeur de la fonction objectif, c'est-à -dire pour que la probabilité de victoire soit influencée par la valeur de la fonction objectif, et plus la différence entre les deux concurrents est grande, plus les chances de gagner le meilleur individu sont grandes, et avec des valeurs presque égales de la fonction objectif, la probabilité de victoire d'un individu serait proche de 50%.
Type Cross - Crossing
AprÚs la formation de «familles» d'individus, pour le croisement, vous devez traverser leurs chromosomes pour obtenir des enfants avec de nouveaux chromosomes. L'étape de croisement est représentée par le type Cross , qui est déclaré comme suit:
pub trait Cross<T: Clone> { fn cross(&mut self, parents: &[&T]) -> Vec<T>; }
La mĂ©thode cross () croise un ensemble de parents. Cette mĂ©thode accepte le paramĂštre parents - une tranche de rĂ©fĂ©rences aux chromosomes des parents (pas les individus eux-mĂȘmes) - et devrait renvoyer un vecteur des nouveaux chromosomes. La taille des parents dĂ©pend de l'algorithme de sĂ©lection des partenaires Ă croiser Ă l'aide du trait Pairing , dĂ©crit dans la section prĂ©cĂ©dente. Souvent utilisĂ© un algorithme qui crĂ©e de nouveaux chromosomes basĂ©s sur les chromosomes de deux parents, alors la taille des parents sera Ă©gale Ă deux.
Le module optlib :: génétique :: cross contient des structures pour lesquelles le type Cross est implémenté avec différents algorithmes de croisement.
- VecCrossAllGenes - une structure conçue pour traverser les chromosomes, lorsque chaque individu a un ensemble de chromosomes - c'est un vecteur. Le constructeur VecCrossAllGenes accepte un type d'objet Cross qui sera appliquĂ© Ă tous les Ă©lĂ©ments des vecteurs chromosomiques parents. Dans ce cas, la taille des vecteurs chromosomiques parents doit ĂȘtre de la mĂȘme taille. L'exemple ci-dessus utilise la structure VecCrossAllGenes car le chromosome des individus utilisĂ©s est de type Vec <f32>.
- CrossMean est une structure qui croise deux nombres de telle maniÚre qu'en tant que chromosome fille, il y aura un nombre calculé comme la moyenne arithmétique des chromosomes parents.
- FloatCrossGeometricMean est une structure qui croise deux nombres de telle maniÚre qu'en tant que chromosome fille, il y aura un nombre calculé comme la moyenne géométrique des chromosomes parents. Il devrait y avoir des nombres à virgule flottante sous forme de chromosomes.
- CrossBitwise - croisement au point unique au niveau du bit de deux nombres.
- FloatCrossExp - croisement ponctuel au niveau du bit de nombres à virgule flottante. La mantisse indépendante et les exposants parentaux se croisent. L'enfant reçoit le signe de l'un des parents.
Le
module optlib :: génétique :: cross contient
également des fonctions pour les nombres croisés au niveau du bit de différents types - entier et virgule flottante.
Type Mutation - Mutation
AprÚs le croisement, il est nécessaire de muter les enfants obtenus afin de maintenir la diversité des chromosomes, et la population n'est pas passée à un état dégénéré. Pour la population, le trait Mutation est prévu, qui est déclaré comme suit:
pub trait Mutation<T: Clone> { fn mutation(&mut self, chromosomes: &T) -> T; }
La seule méthode de mutation () du trait Mutation prend une référence à un chromosome et devrait renvoyer un chromosome éventuellement altéré. En rÚgle générale, une faible probabilité de mutation est établie, par exemple, quelques pour cent, de sorte que les chromosomes obtenus sur la base des parents restent, et améliorent ainsi la population.
Certains algorithmes de mutation sont déjà implémentés dans le module optlib :: génétique :: mutation .
Ce module, par exemple, contient la structure VecMutation , qui fonctionne de maniÚre similaire à la structure VecCrossAllGenes , c'est-à -dire si le chromosome est un vecteur, VecMutation accepte le type d'objet Mutation et l'applique avec une probabilité donnée pour tous les éléments du vecteur, créant un nouveau vecteur avec des gÚnes mutés (éléments du vecteur chromosome).
Le module optlib :: génétique :: mutation contient également une structure BitwiseMutation pour laquelle le trait Mutation est implémenté. Cette structure inverse un nombre donné de bits aléatoires dans le chromosome qui lui est transmis. Il est conseillé d'utiliser cette structure avec la structure VecMutation.
Trait pré-naissance - présélection
AprĂšs le croisement et la mutation, la phase de sĂ©lection se produit gĂ©nĂ©ralement, au cours de laquelle les individus les plus infructueux sont dĂ©truits. Cependant, dans la mise en Ćuvre de l'algorithme gĂ©nĂ©tique dans la bibliothĂšque gĂ©nĂ©tique optlib ::, avant l'Ă©tape de sĂ©lection, il existe une autre Ă©tape Ă laquelle les chromosomes infructueux peuvent ĂȘtre Ă©liminĂ©s. Cette Ă©tape a Ă©tĂ© ajoutĂ©e car le calcul de la fonction objectif pour les individus prend souvent beaucoup de temps, et il n'est pas nĂ©cessaire de le calculer si les chromosomes ne tombent pas dans l'intervalle de recherche connu. Par exemple, dans l'exemple ci-dessus, les chromosomes qui ne tombent pas sur le segment [-500; 500].
Pour effectuer un tel préfiltrage, le trait PreBirth est prévu , qui est déclaré comme suit:
pub trait PreBirth<T: Clone> { fn pre_birth(&mut self, population: &Population<T>, new_chromosomes: &mut Vec<T>); }
La seule mĂ©thode preBirth () du trait PreBirth est une rĂ©fĂ©rence Ă la structure de la population mentionnĂ©e ci-dessus, ainsi qu'une rĂ©fĂ©rence mutable au vecteur chromosomique new_chromosomes. Il s'agit du vecteur de chromosomes obtenu aprĂšs l'Ă©tape de mutation. La mise en Ćuvre de la mĂ©thode pre_birth () devrait supprimer les chromosomes de ce vecteur qui ne sont Ă©videmment pas adaptĂ©s Ă l'Ă©tat du problĂšme.
Pour le cas oĂč la fonction du vecteur de nombres Ă virgule flottante est optimisĂ©e, le module optlib :: gĂ©nĂ©tique :: pre_birth :: vec_float contient la structure CheckChromoInterval , conçue pour supprimer les chromosomes s'ils sont un vecteur de nombres Ă virgule flottante et un Ă©lĂ©ment du vecteur ne tombe pas dans l'intervalle spĂ©cifiĂ©.
Le constructeur de la structure CheckChromoInterval est le suivant:
pub fn new(intervals: Vec<(G, G)>) -> CheckChromoInterval<G> { ... }
Ici, le constructeur prend un vecteur de tuples avec deux éléments - la valeur minimale et maximale pour chaque élément du chromosome. Ainsi, la taille du vecteur d'intervalles doit coïncider avec la taille du vecteur chromosomique des individus. Dans l'exemple ci-dessus, un vecteur de 15 tuples (-500.0f32, 500.0f32) a été utilisé comme intervalles.
Sélection Sélection - Sélection
AprÚs une sélection préliminaire des chromosomes, des individus sont créés et placés dans la population (Structure de la population ). Dans le processus de création d'individus pour chacun d'eux, la valeur de la fonction objectif est calculée. Au stade de la sélection, il faut détruire un certain nombre d'individus pour que la population ne croisse pas indéfiniment. Pour cela, le trait Selection est utilisé, qui est déclaré comme suit:
pub trait Selection<T: Clone> { fn kill(&mut self, population: &mut Population<T>); }
Un objet qui implĂ©mente le trait Selection dans la mĂ©thode kill () doit appeler la mĂ©thode kill () de la structure individuelle (individu) pour chaque individu de la population qui doit ĂȘtre dĂ©truit.
Pour contourner tous les individus d'une population, vous pouvez utiliser un itérateur qui renvoie la méthode IterMut () de la structure de la population.
Puisqu'il est souvent nécessaire d'appliquer plusieurs critÚres de sélection, lors de la création de la structure GeneticOptimizer , le constructeur accepte un vecteur d'objets de type Sélection.
Comme pour les autres étapes de l'algorithme génétique, le module optlib :: génétique :: sélection propose déjà plusieurs implémentations de l'étape de sélection.
Trait StopChecker - critĂšre d'arrĂȘt
AprĂšs l'Ă©tape de sĂ©lection, vous devez vĂ©rifier s'il est temps d'arrĂȘter l'algorithme gĂ©nĂ©tique. Comme dĂ©jĂ mentionnĂ© ci-dessus, Ă ce stade, vous pouvez Ă©galement utiliser diffĂ©rents algorithmes d'arrĂȘt. Pour l'interruption de l'algorithme, l'objet est responsable de la mise en Ćuvre du trait StopChecker , qui est dĂ©clarĂ© comme suit:
pub trait StopChecker<T: Clone> { fn can_stop(&mut self, population: &Population<T>) -> bool; }
Ici, la mĂ©thode can_stop () devrait retourner true si l'algorithme peut ĂȘtre arrĂȘtĂ©, sinon cette mĂ©thode devrait retourner false.
Le module optlib :: gĂ©nĂ©tique :: stopchecker contient plusieurs structures avec des critĂšres d'arrĂȘt de base et deux structures pour crĂ©er des combinaisons Ă partir d'autres critĂšres:
- MaxIterations - critĂšre de rupture par numĂ©ro de gĂ©nĂ©ration. L'algorithme gĂ©nĂ©tique s'arrĂȘtera aprĂšs un nombre donnĂ© d'itĂ©rations (gĂ©nĂ©rations).
- GoalNotChange - un critĂšre de rupture selon lequel l'algorithme doit s'arrĂȘter si une nouvelle solution ne peut pas ĂȘtre trouvĂ©e pour un nombre donnĂ© d'itĂ©rations. Ou en d'autres termes, si pour un nombre donnĂ© de gĂ©nĂ©rations, il n'y a pas d'individus plus performants.
- Seuil - un critĂšre d'arrĂȘt selon lequel l'algorithme gĂ©nĂ©tique est interrompu si la valeur de la fonction objective de la meilleure solution Ă l'heure actuelle est infĂ©rieure Ă la valeur seuil spĂ©cifiĂ©e.
- CompositeAll â , ( - StopChecker). , - , .
- CompositeAny â , ( - StopChecker). , - , .
Logger â
Logger , , , . Logger :
pub trait Logger<T: Clone> {
optlib::genetic::logging , Logger:
Le constructeur de la structure GeneticOptimizer , comme dernier argument, accepte un vecteur de types de caractĂšres Logger, qui vous permet de configurer de maniĂšre flexible la sortie du programme.Fonctions de test d'algorithmes d'optimisation
Fonction Schweffel
Pour tester les algorithmes d'optimisation, de nombreuses fonctions ont Ă©tĂ© inventĂ©es avec de nombreux extrema locaux. Le module optlib :: testfunctions contient plusieurs fonctions sur lesquelles des algorithmes peuvent ĂȘtre testĂ©s. Au moment d'Ă©crire ces lignes, le module optlib :: testfunctions ne contient que deux fonctions.
L'une de ces fonctions est la fonction Schweffel, qui a été décrite au tout début de l'article. Encore une fois, je rappelle que cette fonction s'écrit comme suit:
x' = (420.9687, 420.9687, ...). .
optlib::testfunctions schwefel . N .
, , , , .
:
x' = (1.0, 2.0,⊠N). .
optlib::testfunctions paraboloid .
Conclusion
optlib , . ( optlib::genetic ), , , , .
optlib::genetic. . , , , , .
, . , ( , ..)
De plus, il est prévu d'ajouter de nouvelles fonctions analytiques (en plus de la fonction Schweffel) pour tester les algorithmes d'optimisation.
Encore une fois, je rappelle les liens associés à la bibliothÚque optlib:
J'attends vos commentaires, ajouts et commentaires avec impatience.