Serie : Search / Partie 1 — Fondations (C#). Jumeau .NET du notebook Python Search-11-Metaheuristics.
Duree estimee : ~1h30
Prerequis : Search-04-LocalSearch (recuit simule, hill-climbing), notions d’algebre lineaire et de C# / .NET.
Pourquoi un jumeau C# ? Le notebook Python original s’appuie sur la librairie mealpy (PSO, ABC, SA, BRO, GWO, WOA, GA, DE) et numpy/matplotlib pour comparer des métaheuristiques sur des fonctions de benchmark. Ce jumeau reimplemente les algorithmes majeurs depuis zero en C# .NET 9 (coeur BCL pur, 0 NuGet pour le from-scratch) : essaim particulaire (PSO), recuit simule (SA), algorithme génétique (GA). Une Tranche 2 (§10) confronte ensuite ce coeur from-scratch à la librairie SOTA GeneticSharp, pour une parité lib-vs-lib avec le jumeau Python mealpy. Les fonctions de benchmark (Sphere, Rastrigin, Rosenbrock, Ackley) sont codees explicitement, ainsi que la convergence, l’analyse de paramètres et un exemple d’optimisation de profit. Les graphiques matplotlib sont remplacés par des tables et traces ASCII. Les deux notebooks derivent les mêmes optima (f=0 sur les benchmarks, profit max sur l’exemple entreprise).
1. Introduction aux métaheuristiques
Une métaheuristique est une stratégie d’optimisation generique qui guide une recherche dans un espace de solutions trop vaste pour une exploration exhaustive. Contrairement aux méthodes exactes (programmation lineaire, branch-and-bound), elle ne garantit pas l’optimalite mais trouve de bonnes solutions en temps raisonnable sur des problemes reels (dimension elevee, multimodalite, non-differentiabilite).
Le compromis central est exploration vs exploitation : - Exploration : visiter de nouvelles regions de l’espace (eviter les minima locaux). - Exploitation : affiner autour des meilleures solutions trouvees.
On teste les métaheuristiques sur 4 fonctions classiques dont l’optimum global est connu. Elles couvrent les principaux pieges (unimodalite, multimodalite, vallee etroite, plateaux).
// Fonctions de benchmark classiques. Toutes ont un optimum global f=0.using System.Linq;// Sphere : unimodale, convexe. Optimum x*=[0,...,0], f=0.staticdoubleSphere(double[] x)=> x.Select(v => v*v).Sum();// Rastrigin : multimodale avec de nombreux optima locaux. Optimum x*=[0,...,0], f=0.staticdoubleRastrigin(double[] x){double A =10.0;int n = x.Length;double s =0;foreach(double v in x) s += v*v - A*Math.Cos(2*Math.PI*v);return A*n + s;}// Rosenbrock : vallee etroite et incurvee. Optimum x*=[1,...,1], f=0.staticdoubleRosenbrock(double[] x){double s =0;for(int i =0; i < x.Length-1; i++) s +=100.0*(x[i+1]- x[i]*x[i])*(x[i+1]- x[i]*x[i])+(1- x[i])*(1- x[i]);return s;}// Ackley : multimodale avec plateaux. Optimum x*=[0,...,0], f=0.staticdoubleAckley(double[] x){double a =20.0, b =0.2, c =2*Math.PI;int d = x.Length;double sum1 = x.Select(v => v*v).Sum();double sum2 = x.Select(v => Math.Cos(c*v)).Sum();return-a*Math.Exp(-b*Math.Sqrt(sum1/d))- Math.Exp(sum2/d)+ a + Math.E;}// Tableau recapitulatifConsole.WriteLine("Fonctions de benchmark :");Console.WriteLine($" {"Nom",-12} {"Type",-16} {"Optimum",-20}");Console.WriteLine(newstring('-',50));Console.WriteLine($" {"Sphere",-12} {"Unimodale",-16} {"[0,...,0], f=0",-20}");Console.WriteLine($" {"Rastrigin",-12} {"Multimodale",-16} {"[0,...,0], f=0",-20}");Console.WriteLine($" {"Rosenbrock",-12} {"Vallee etroite",-16} {"[1,...,1], f=0",-20}");Console.WriteLine($" {"Ackley",-12} {"Multimodale",-16} {"[0,...,0], f=0",-20}");// Verif des optimadouble[] zero =newdouble[5];double[] one = Enumerable.Repeat(1.0,5).ToArray();Console.WriteLine($"\nVerif : Sphere(0)={Sphere(zero):F6} Rastrigin(0)={Rastrigin(zero):F6} Rosenbrock(1)={Rosenbrock(one):F6} Ackley(0)={Ackley(zero):F6} (tous attendus ~0)");
Lecture des quatre paysages : chaque fonction est un diagnostic
Le tableau de sortie n’est pas une liste : c’est une trousse d’outils de diagnostic. Sphere (unimodale, convexe) : tout marche, y compris un gradient — elle sert de sanity check. Rastrigin (multimodale : cosinus superposés à la sphère) : piège à bassins secondaires — elle teste la capacité à échapper aux optima locaux. Rosenbrock (vallée parabolique étroite) : le gradient existe partout mais zigzague d’une paroi à l’autre — elle teste la progression dans un couloir. Ackley (plateau quasi plat + puits central) : presque aucun signal de pente — elle teste l’exploration quand le paysage ne guide pas. À chaque paysage son échec caractéristique : c’est pour cela qu’aucun algorithme ne domine le tableau final du §6.
3. Particle Swarm Optimization (PSO)
Inspiré du comportement collectif des bancs de poissons / vols d’oiseaux. Chaque particule est une solution candidate qui se déplace dans l’espace selon sa vitesse. À chaque itération, la vitesse combine trois termes : - Inertie (w) : tendance à conserver sa direction. - Cognitif (c1) : attraction vers la meilleure position personnelle (pbest). - Social (c2) : attraction vers la meilleure position globale (gbest).
PSO sur Rastrigin (dim=10, 300 epochs, 50 particules) :
Solution (4 premiers) : [0,0002, -0,0000, 0,0001, 0,9950, ...]
Objectif : 5,969905
Temps : 9,5 ms
Optimal attendu : [0,...,0], f=0
Lecture de la solution : 0,9950 dans le préfixe imprimé, ~6 dimensions piégées
Le préfixe imprimé [0,0002, -0,0000, 0,0001, 0,9950, ...] ne montre que 4 des 10 coordonnées : trois quasi nulles, une collée à 0,995, soit ~1,0 — un minimum secondaire du cosinus de Rastrigin (les minima secondaires sont à ~1,0 de l’axe). L’objectif 5,969905 dit la suite : à ~1 par dimension piégée, 5,97 ≈ 6 × 1 — environ six des dix dimensions sont coincées dans des minima secondaires, dont cinq invisibles dans ce préfixe (la cellule n’imprime que les 4 premières). C’est l’échec partiel typique du PSO : convergence intra-bassin excellente (les dimensions libres retombent à ~0), basculement inter-bassins manqué pour les autres.
Convergence PSO
Plus le nombre d’epochs augmente, plus l’essaim affine sa solution. Sur Rastrigin (multimodale), on observe une décroissance rapide puis un plateau — caractéristique de l’équilibre exploration/exploitation.
// Convergence : UN seul run PSO, on enregistre le gbest aux epochs [10,20,50,100,200].// (La vraie courbe de convergence = trajectoire du meilleur trouvé, forcement decroissante.)int[] checkpoints ={10,20,50,100,200};int totalEp = checkpoints[^1];var rngC =newRandom(42);int dimC =10;double lbC =-5.12, ubC =5.12;int popC =30;double wMaxC =0.9, wMinC =0.4, c1C =2.0, c2C =2.0;// Init particulesvar posC =newdouble[popC][];var velC =newdouble[popC][];var pbestC =newdouble[popC][];var pbestFitC =newdouble[popC];double[] gbestC =newdouble[dimC];double gbestFitC =double.MaxValue;for(int p =0; p < popC; p++){ posC[p]=newdouble[dimC]; velC[p]=newdouble[dimC]; pbestC[p]=newdouble[dimC];for(int d =0; d < dimC; d++) posC[p][d]= lbC + rngC.NextDouble()*(ubC-lbC); pbestFitC[p]=Rastrigin(posC[p]); pbestC[p]=(double[])posC[p].Clone();if(pbestFitC[p]< gbestFitC){ gbestFitC = pbestFitC[p]; gbestC =(double[])pbestC[p].Clone();}}int cpIdx =0;Console.WriteLine($"{"Epoch",-8} {"gbest fitness",-16} {"Amelioration",-14}");Console.WriteLine(newstring('-',40));double firstG =-1;for(int it =1; it <= totalEp; it++){double wIt = wMaxC -(wMaxC-wMinC)*it/totalEp;for(int p =0; p < popC; p++)for(int d =0; d < dimC; d++){double r1 = rngC.NextDouble(), r2 = rngC.NextDouble();double vmax =0.2*(ubC-lbC); velC[p][d]= Math.Max(-vmax, Math.Min(vmax, wIt*velC[p][d]+ c1C*r1*(pbestC[p][d]-posC[p][d])+ c2C*r2*(gbestC[d]-posC[p][d]))); posC[p][d]= Math.Max(lbC, Math.Min(ubC, posC[p][d]+ velC[p][d]));}for(int p =0; p < popC; p++){double fit =Rastrigin(posC[p]);if(fit < pbestFitC[p]){ pbestFitC[p]= fit; pbestC[p]=(double[])posC[p].Clone();}if(fit < gbestFitC){ gbestFitC = fit; gbestC =(double[])posC[p].Clone();}}if(cpIdx < checkpoints.Length&& it == checkpoints[cpIdx]){if(firstG <0) firstG = gbestFitC;double ratio = gbestFitC >1e-9? firstG/gbestFitC :double.PositiveInfinity; Console.WriteLine($"{it,-8} {gbestFitC,-16:F6} {ratio,-14:F1}x"); cpIdx++;}}Console.WriteLine("\nLa fitness gbest decroit monotone : exploration globale rapide, puis raffinement fin du bassin optimal.");
Epoch gbest fitness Amelioration
----------------------------------------
10 69,258704 1,0 x
20 57,704372 1,2 x
50 44,423136 1,6 x
100 27,319939 2,5 x
200 1,064067 65,1 x
La fitness gbest decroit monotone : exploration globale rapide, puis raffinement fin du bassin optimal.
Variabilite stochastique — pourquoi un seul run ne suffit pas
La cellule precedente trace la decroissance de gbest pour un seul run (seed 42) : la courbe est monotone, ce qui suggere une convergence “propre”. C’est trompeur. PSO est stochastique : l’initialisation de l’essaim et les tirages aleatoires (r1, r2) changent a chaque execution. Sur Rastrigin (paysage multimodal, crible d’optima locaux), deux executions peuvent converger vers des bassins differents — l’une proche de l’optimum global (f->0), l’autre piegee dans un optimum local (f=5 a 30).
La conclusion robuste n’est donc jamais “PSO a trouve f=2.3”, mais “PSO trouve f=2.3 en moyenne sur N seeds, avec un ecart-type de X”. C’est pourquoi toute etude serieuse de metaheuristique reporte mean +/- std sur plusieurs seeds — un standard en optimisation stochastique (et la convention multi-seed exige des notebooks ML/trading, cf. regle de revue section C).
// Multi-seed : 8 runs PSO independants sur Rastrigin (meme config que cellule 6).// On releve la fitness finale de chaque seed pour reveler la variabilite cachee par un run unique.int[] seedsMS ={1,2,3,4,5,6,7,8};var finals =new List<double>();Console.WriteLine($"PSO sur Rastrigin (dim=10, 200 epochs, 30 particules) - 8 seeds independants");Console.WriteLine($"{"Seed",-6} {"gbest final",-14} {"lecture"}");Console.WriteLine(newstring('-',44));foreach(int s in seedsMS){var p =newPSO(dim:10, lb:-5.12, ub:5.12, f: Rastrigin, popSize:30, epochs:200, seed: s);var(_, fit, _)= p.Solve(); finals.Add(fit);string lecture = fit <1.0?"proche optimum global":(fit <10.0?"bassin secondaire":"piege local"); Console.WriteLine($"{s,-6} {fit,-14:F4} {lecture}");}Console.WriteLine(newstring('-',44));double mean = finals.Average();double std = Math.Sqrt(finals.Average(v =>(v - mean)*(v - mean)));Console.WriteLine($"Moyenne = {mean:F3} Ecart-type = {std:F3} [min {finals.Min():F3}, max {finals.Max():F3}]");Console.WriteLine($"\nL'ecart-type ({std:F2}) confirme la dispersion : un seul run (seed 42 ci-dessus)");Console.WriteLine("ne representative pas la distribution reelle des issues. D'ou l'obligation du multi-seed.");
PSO sur Rastrigin (dim=10, 200 epochs, 30 particules) - 8 seeds independants
Seed gbest final lecture
--------------------------------------------
1 6,1629 bassin secondaire
2 6,5554 bassin secondaire
3 4,3139 bassin secondaire
4 5,1778 bassin secondaire
5 3,1368 bassin secondaire
6 7,9811 bassin secondaire
7 2,9884 bassin secondaire
8 5,2554 bassin secondaire
--------------------------------------------
Moyenne = 5,196 Ecart-type = 1,598 [min 2,988, max 7,981]
L'ecart-type (1,60) confirme la dispersion : un seul run (seed 42 ci-dessus)
ne representative pas la distribution reelle des issues. D'ou l'obligation du multi-seed.
Lecture multi-seed : aucun run ne trouve le global — et c’est la vraie information
Les 8 seeds finissent tous en « bassin secondaire » (2,99 à 7,98, moyenne 5,20, écart-type 1,60) : aucun n’atteint 0. La sortie est honnête là où un rapport mono-run masquerait tout : la variance inter-runs (σ = 1,6 sur une moyenne de 5,2, soit ±31 %) est assez grande pour qu’un seed chanceux et un rapport unique « prouvent » n’importe quoi. C’est la même discipline que les expériences ML multi-seed du repo : un résultat stochastique non répété n’est pas un résultat. Notez aussi la cohérence avec la cellule précédente (convergence monotone dans un bassin) : le multi-seed montre que la question n’est pas la vitesse mais quel bassin.
4. Simulated Annealing (SA)
Inspiré du recuit métallurgique : un métal chauffé puis refroidi lentement atteint un état d’énergie minimale. À haute température, le système accepte facilement des solutions dégradées (exploration) ; à basse température, il devient glouton (exploitation).
L’acceptation d’une solution voisine de coût Δ = f(voisin) - f(courant) : - Si Δ < 0 (meilleure) : toujours acceptée. - Si Δ >= 0 (pire) : acceptée avec probabilité exp(-Δ / T).
La température décroît selon un schéma de refroidissement (T = T0 · α^it, α < 1).
// Simulated Annealing (recuit simule) from-scratch.publicclass SA{publicint Dim;publicdouble Lb, Ub;public Func<double[],double> ObjFunc;publicint Epochs;publicdouble TempInit, Alpha, StepSize;public Random Rng;publicSA(int dim,double lb,double ub, Func<double[],double> f,int epochs,double tempInit =100.0,double alpha =0.995,double stepSize =0.5,int seed =42){ Dim = dim; Lb = lb; Ub = ub; ObjFunc = f; Epochs = epochs; TempInit = tempInit; Alpha = alpha; StepSize = stepSize; Rng =newRandom(seed);}double[]RandomPos(){var x =newdouble[Dim];for(int i =0; i < Dim; i++) x[i]= Lb + Rng.NextDouble()*(Ub - Lb);return x;}public(double[] solution,double fitness,double ms)Solve(){var cur =RandomPos();double curFit =ObjFunc(cur);var best =(double[])cur.Clone();double bestFit = curFit;double T = TempInit;var sw = System.Diagnostics.Stopwatch.StartNew();for(int it =0; it < Epochs; it++){// Voisin gaussien — taille de pas proportionnelle a sqrt(T/T0) :// grand pas a haute T (exploration), petit pas a basse T (raffinement).double stepT = StepSize * Math.Sqrt(Math.Max(T,1e-12)/ TempInit);var cand =(double[])cur.Clone();for(int d =0; d < Dim; d++){// Box-Muller gaussiendouble u1 = Rng.NextDouble(), u2 = Rng.NextDouble();double z = Math.Sqrt(-2*Math.Log(u1))* Math.Cos(2*Math.PI*u2); cand[d]= cur[d]+ stepT * z; cand[d]= Math.Max(Lb, Math.Min(Ub, cand[d]));}double candFit =ObjFunc(cand);double delta = candFit - curFit;if(delta <0|| Rng.NextDouble()< Math.Exp(-delta / T)){ cur = cand; curFit = candFit;if(curFit < bestFit){ bestFit = curFit; best =(double[])cur.Clone();}} T *= Alpha;if(T <1e-12) T =1e-12;} sw.Stop();return(best, bestFit, sw.Elapsed.TotalMilliseconds);}}Console.WriteLine("SA implemente.");
SA implemente.
// SA sur Ackley (dim=10, multimodale avec plateaux).var sa =newSA(dim:10, lb:-32.0, ub:32.0, f: Ackley, epochs:30000, tempInit:50.0, alpha:0.9997, stepSize:3.0, seed:42);var(solSA, fitSA, msSA)= sa.Solve();Console.WriteLine("SA sur Ackley (dim=10, 30000 itérations) :");Console.WriteLine($" Solution (4 premiers) : [{string.Join(",", solSA.Take(4).Select(v => v.ToString("F4")))}, ...]");Console.WriteLine($" Objectif : {fitSA:F6}");Console.WriteLine($" Temps : {msSA:F1} ms");Console.WriteLine($" Optimal attendu : [0,...,0], f=0");
SA sur Ackley (dim=10, 30000 itérations) :
Solution (4 premiers) : [6,0180, -6,9937, 0,7097, 5,0060, ...]
Objectif : 15,844004
Temps : 61,1 ms
Optimal attendu : [0,...,0], f=0
Lecture : pourquoi le recuit échoue sur Ackley (15,84) alors qu’il réussit sur Rosenbrock (3,59)
La solution [6,02, -6,99, 0,71, 5,01, ...] est loin de l’origine sur la plupart des dimensions : l’algorithme erre sur le plateau d’Ackley sans jamais être guidé vers le puits central — la surface quasi plate ne fournit presque aucune différence de fitness entre voisins, et le critère de Metropolis accepte tout indifféremment. En contraste, sur Rosenbrock (§6 : 3,59), chaque déplacement le long de la vallée baisse la fitness : le recuit a une pente à suivre. Le diagnostic est général : le recuit vit du signal local du paysage ; sur un plateau, il se comporte en marche aléatoire refroidie.
5. Genetic Algorithm (GA)
Inspiré de la sélection naturelle. Une population de solutions évolue par : - Sélection : les meilleurs individus sont plus susceptibles de se reproduire (tournoi). - Croisement (crossover) : deux parents produisent un enfant mélangeant leurs gènes. - Mutation : perturbation aléatoire pour maintenir la diversité.
Paramètres : taille de population, probabilité de croisement pc, probabilité de mutation pm.
// Genetic Algorithm from-scratch : sélection par tournoi, croisement arithmetique, mutation gaussienne.publicclass GA{publicint Dim;publicdouble Lb, Ub;public Func<double[],double> ObjFunc;publicint PopSize, Epochs;publicdouble Pc, Pm, Sigma;public Random Rng;publicGA(int dim,double lb,double ub, Func<double[],double> f,int popSize,int epochs,double pc =0.9,double pm =0.1,double sigma =0.3,int seed =42){ Dim = dim; Lb = lb; Ub = ub; ObjFunc = f; PopSize = popSize; Epochs = epochs; Pc = pc; Pm = pm; Sigma = sigma; Rng =newRandom(seed);}double[]RandomInd(){var x =newdouble[Dim];for(int i =0; i < Dim; i++) x[i]= Lb + Rng.NextDouble()*(Ub - Lb);return x;}public(double[] solution,double fitness,double ms)Solve(){var pop =newdouble[PopSize][];var fit =newdouble[PopSize];for(int p =0; p < PopSize; p++){ pop[p]=RandomInd(); fit[p]=ObjFunc(pop[p]);}var best =(double[])pop[0].Clone();double bestFit = fit[0];for(int p =1; p < PopSize; p++)if(fit[p]< bestFit){ bestFit = fit[p]; best =(double[])pop[p].Clone();}var sw = System.Diagnostics.Stopwatch.StartNew();for(int it =0; it < Epochs; it++){var newPop =newdouble[PopSize][]; newPop[0]=(double[])best.Clone();// elitisme : on garde le meilleurfor(int p =1; p < PopSize; p++){// Tournoi (taille 3)int i1 =Tournament(pop, fit), i2 =Tournament(pop, fit);var child =(double[])pop[i1].Clone();if(Rng.NextDouble()< Pc)for(int d =0; d < Dim; d++) child[d]=0.5*(pop[i1][d]+ pop[i2][d]);// croisement arithmetique// Mutation gaussiennefor(int d =0; d < Dim; d++)if(Rng.NextDouble()< Pm){double u1 = Rng.NextDouble(), u2 = Rng.NextDouble();double z = Math.Sqrt(-2*Math.Log(u1))* Math.Cos(2*Math.PI*u2); child[d]+= Sigma * z; child[d]= Math.Max(Lb, Math.Min(Ub, child[d]));} newPop[p]= child;}for(int p =1; p < PopSize; p++){ pop[p]= newPop[p]; fit[p]=ObjFunc(pop[p]);if(fit[p]< bestFit){ bestFit = fit[p]; best =(double[])pop[p].Clone();}}} sw.Stop();return(best, bestFit, sw.Elapsed.TotalMilliseconds);}intTournament(double[][] pop,double[] fit,int k =3){int best = Rng.Next(PopSize);for(int t =1; t < k; t++){int c = Rng.Next(PopSize);if(fit[c]< fit[best]) best = c;}return best;}}Console.WriteLine("GA implemente.");
GA implemente.
// GA sur Rosenbrock (dim=10, vallee etroite).var ga =newGA(dim:10, lb:-5.0, ub:5.0, f: Rosenbrock, popSize:50, epochs:200, seed:42);var(solGA, fitGA, msGA)= ga.Solve();Console.WriteLine("GA sur Rosenbrock (dim=10, 200 generations) :");Console.WriteLine($" Solution (4 premiers) : [{string.Join(",", solGA.Take(4).Select(v => v.ToString("F4")))}, ...]");Console.WriteLine($" Objectif : {fitGA:F6}");Console.WriteLine($" Temps : {msGA:F1} ms");Console.WriteLine($" Optimal attendu : [1,...,1], f=0");
GA sur Rosenbrock (dim=10, 200 generations) :
Solution (4 premiers) : [0,3682, 0,1484, 0,0275, 0,0095, ...]
Objectif : 8,068804
Temps : 5,1 ms
Optimal attendu : [1,...,1], f=0
Lecture de la solution GA : des coordonnées qui décroissent vers 0 au lieu de converger vers 1
La solution [0,368, 0,148, 0,028, 0,0095, ...] est étrange à première vue : Rosenbrock veut toutes les coordonnées à 1, mais celles-ci décroissent vers 0 au fil des indices. C’est la signature du croisement arithmétique : la moyenne de deux parents rapproche chaque gène du centroïde de la population ; la population s’effondre vers l’intérieur de son nuage initial avant que la sélection n’ait le temps de tirer vers la vallée. La vallée étroite de Rosenbrock (chaque coordonnée doit suivre la précédente au carré) est impitoyable à ce rétrécissement prématuré : la diversité disparaît avant la découverte du couloir. C’est le compromis exploration/exploitation vu de l’intérieur.
6. Benchmark comparatif
On compare PSO, SA et GA sur les 4 fonctions de benchmark (dim=10), en mesurant la fitness finale atteinte. Chaque algorithme a ses forces : PSO excelle sur les multimodales « structurées », SA explore bien les plateaux, GA est robuste sur les vallées.
// Benchmark comparatif : PSO vs SA vs GA sur les 4 fonctions (dim=10).var benchmarks =new(string name, Func<double[],double> f,double lb,double ub)[]{("Sphere", Sphere,-10.0,10.0),("Rastrigin", Rastrigin,-5.12,5.12),("Rosenbrock", Rosenbrock,-5.0,5.0),("Ackley", Ackley,-32.0,32.0),};Console.WriteLine($"{"Fonction",-12} {"PSO",-12} {"SA",-12} {"GA",-12}");Console.WriteLine(newstring('-',50));foreach(var(name, f, lb, ub)in benchmarks){var rp =newPSO(10, lb, ub, f,50,200, seed:42).Solve();var rs =newSA(10, lb, ub, f,30000, tempInit:50, alpha:0.9997, stepSize:(ub-lb)/8.0, seed:42).Solve();var rg =newGA(10, lb, ub, f,50,200, seed:42).Solve(); Console.WriteLine($"{name,-12} {rp.fitness,-12:F4} {rs.fitness,-12:F4} {rg.fitness,-12:F4}");}Console.WriteLine("\nTendances : Sphere (unimodale) = facile pour tous ; Rastrigin/Ackley (multimodales) = PSO souvent au-dessus ; Rosenbrock (vallee) = GA robuste.");
Fonction PSO SA GA
--------------------------------------------------
Sphere 0,0000 0,0065 0,0002
Rastrigin 4,9968 39,8989 8,5265
Rosenbrock 6,5656 3,5938 8,0688
Ackley 0,0122 0,2686 6,3691
Tendances : Sphere (unimodale) = facile pour tous ; Rastrigin/Ackley (multimodales) = PSO souvent au-dessus ; Rosenbrock (vallee) = GA robuste.
Lecture du tableau final : PSO devant sur trois fonctions, SA dans la vallée
PSO gagne trois des quatre : Sphere (0,0000, devant le GA à 0,0002), Rastrigin (4,99) et Ackley (0,012) ; SA gagne Rosenbrock (3,59) — et perd Rastrigin de façon catastrophique (39,9, cohérent avec la lecture du plateau ci-dessus). Le GA maison est dominé sur les quatre fonctions à ce réglage. Chaque colonne reflète la mécanique profonde vue section par section : l’essaim (information sociale partagée) couvre bien les multimodales comme l’unimodale ; le recuit (descente + acceptation d’exception) suit bien les vallées continues ; le GA (codage continu, croisement arithmétique) n’a ici aucun terrain d’avantage. La leçon opérationnelle : on choisit un algorithme par diagnostic du paysage, pas par préférence — et on le vérifie en multi-seed (§3).
7. Analyse de paramètres — impact de la taille de population
La taille de population est un compromis : trop petite = convergence prématurée (minimum local) ; trop grande = lenteur. On mesure la fitness finale et le temps de calcul pour PSO sur Rastrigin avec différentes tailles.
// Impact de la taille de population sur PSO (Rastrigin, dim=10).// 5 seeds par taille, on reporte la meilleure fitness (robuste au bruit stochastique).int[] popSizes ={10,30,50,100};int[] seedsPA ={0,42,123,456,789};Console.WriteLine($"{"PopSize",-10} {"Meilleure",-14} {"Moyenne",-14} {"Temps(ms)",-12}");Console.WriteLine(newstring('-',50));foreach(int pop in popSizes){double best =double.MaxValue, sum =0;double ms =0;foreach(int sd in seedsPA){var r =newPSO(10,-5.12,5.12, Rastrigin, pop,200, seed: sd).Solve();if(r.fitness< best) best = r.fitness; sum += r.fitness; ms += r.ms;} Console.WriteLine($"{pop,-10} {best,-14:F6} {sum/seedsPA.Length,-14:F6} {ms/seedsPA.Length,-12:F1}");}Console.WriteLine("\nUne population plus large ameliore la meilleure fitness atteinte (plus d'exploration), au prix du temps (lineaire en popSize).");
PopSize Meilleure Moyenne Temps (ms)
--------------------------------------------------
10 6,767508 10,904264 1,2
30 4,991659 7,090220 3,6
50 3,019542 4,873531 6,1
100 2,113923 6,225824 12,4
Une population plus large ameliore la meilleure fitness atteinte (plus d'exploration), au prix du temps (lineaire en popSize).
Lecture des deux colonnes : la meilleure s’améliore, la moyenne n’est pas monotone
De 10 à 100 particules, la meilleure fitness passe de 6,77 à 2,11 (plus d’exploration = plus de chances de tomber dans un bon bassin) et le temps croît linéairement (1,2 à 12,4 ms). Mais la colonne moyenne fait autre chose : 10,90, 7,09, 4,87, puis 6,23 — elle rebondit à 100. Une grande population n’est pas uniformément meilleure : elle multiplie aussi les essaims qui convergent mal (plus de particules libres de se disperser sur des bassins secondaires), à budget par particule identique. Si vous ne rapportiez que la meilleure colonne, vous concluriez « 100 gagne » ; la moyenne dit « 50 est le meilleur compromis coût/robustesse ». Les deux colonnes ensemble, encore une fois, sont l’information.
8. Exemple guide : optimisation de profit entreprise
Une entreprise maximise un profit P(x,y) = 50x + 80y - x² - 2y² - xy sous contraintes x,y ∈ [0,20]. On dérive analytiquement l’optimum (annulation du gradient) : - ∂P/∂x = 50 - 2x - y = 0 - ∂P/∂y = 80 - 4y - x = 0
D’où x ≈ 17.143, y ≈ 15.714, P ≈ 1057.1. On vérifie que PSO retrouve ce même optimum (en minimisant -P).
// Exemple guide : maximiser profit = 50x + 80y - x^2 - 2y^2 - xy.// On minimise l'oppose (convention metaheuristique).staticdoubleProfitNeg(double[] sol){double x = sol[0], y = sol[1];double profit =50*x +80*y - x*x -2*y*y - x*y;return-profit;}// Optimum analytique : x=120/7 ~ 17.1429, y=110/7 ~ 15.7143, profit ~ 1057.14double xOpt =120.0/7, yOpt =110.0/7;double profitOpt =50*xOpt +80*yOpt - xOpt*xOpt -2*yOpt*yOpt - xOpt*yOpt;Console.WriteLine($"Optimum analytique : x={xOpt:F4}, y={yOpt:F4}, profit={profitOpt:F4}");Console.WriteLine();// PSO sur le profit (bornes [0,20])var psoProfit =newPSO(dim:2, lb:0.0, ub:20.0, f: ProfitNeg, popSize:30, epochs:100, seed:42);var(solP, fitP, msP)= psoProfit.Solve();Console.WriteLine($"PSO : x={solP[0]:F4}, y={solP[1]:F4}, profit={-fitP:F4} ({msP:F1} ms)");// SA sur le mêmevar saProfit =newSA(dim:2, lb:0.0, ub:20.0, f: ProfitNeg, epochs:3000, tempInit:50, alpha:0.999, stepSize:3.0, seed:42);var(solS, fitS, msS)= saProfit.Solve();Console.WriteLine($"SA : x={solS[0]:F4}, y={solS[1]:F4}, profit={-fitS:F4} ({msS:F1} ms)");Console.WriteLine();Console.WriteLine($"Concordance avec l'optimum analytique (x={xOpt:F3}, y={yOpt:F3}, profit={profitOpt:F3}) : les metaheuristiques le retrouvent.");// --- Profit SAISONNIER : quand le metaheuristique devient necessaire ---// Le profit concave ci-dessus est unimodal : un seul optimum, accessible analytiquement,// que PSO et SA retrouvent trivialement (une descente de gradient ferait l'affaire).// Un marche reel est cyclique (saisonnalite). Ajoutons une composante sinusoidale :// P(x,y) = 50x + 80y - x^2 - 2y^2 - xy + 120*sin(1.2x)*sin(1.2y)// Le terme sinusoidal grave un quadrillage de ~15 maxima locaux aux bassins profonds dans// [0,20]^2. L'optimum n'est plus accessible analytiquement (gradient = 0 = systeme non// lineaire sans solution fermee), et une descente de gradient resterait coincee dans le// premier bassin rencontre.// C'est precisement la que PSO (exploration globale par essaim) et SA (echappement des// minima locaux par acceptation probabiliste de mouvements defavorables) deviennent// indispensables - leur capacite distinctive est enfin mise a contribution.staticdoubleProfitSaisonnierNeg(double[] sol){double x = sol[0], y = sol[1];double baseConcave =50*x +80*y - x*x -2*y*y - x*y;double saison =120.0* Math.Sin(1.2*x)* Math.Sin(1.2*y);return-(baseConcave + saison);}Console.WriteLine();Console.WriteLine("Profit saisonnier (multimodal) - PSO vs SA sur 5 seeds :");int[] seedsMM ={0,42,123,456,789};Console.WriteLine($"{"Seed",-6} {"PSO profit",-12} {"SA profit",-12} {"PSO(x,y)",-20} {"SA(x,y)",-20}");Console.WriteLine(newstring('-',72));double psoBestMM =double.MinValue, saBestMM =double.MinValue, psoSumMM =0, saSumMM =0;foreach(int sd in seedsMM){var(solP, fitP, _)=newPSO(2,0.0,20.0, ProfitSaisonnierNeg,30,100, seed: sd).Solve();var(solS, fitS, _)=newSA(2,0.0,20.0, ProfitSaisonnierNeg,3000, tempInit:50, alpha:0.999, stepSize:3.0, seed: sd).Solve();double profP =-fitP, profS =-fitS; psoSumMM += profP; saSumMM += profS;if(profP > psoBestMM) psoBestMM = profP;if(profS > saBestMM) saBestMM = profS;string posP = $"({solP[0]:F2}, {solP[1]:F2})";string posS = $"({solS[0]:F2}, {solS[1]:F2})"; Console.WriteLine($"{sd,-6} {profP,-12:F2} {profS,-12:F2} {posP,-20} {posS,-20}");}Console.WriteLine();Console.WriteLine($"PSO : meilleur profit = {psoBestMM:F2}, moyenne = {psoSumMM/seedsMM.Length:F2}");Console.WriteLine($"SA : meilleur profit = {saBestMM:F2}, moyenne = {saSumMM/seedsMM.Length:F2}");
Lecture du contraste : quand le métaheuristique devient nécessaire
La cellule ci-dessus oppose deux paysages sur le même thème (maximiser un profit d’entreprise) pour montrer précisément quand PSO et SA apportent quelque chose qu’une méthode classique ne pourrait pas.
Profit concave (première partie).P(x,y) = 50x + 80y − x² − 2y² − xy est unimodal : un seul optimum, accessible analytiquement en annulant le gradient (x ≈ 17,14, y ≈ 15,71, P ≈ 1057). PSO et SA le retrouvent en une fraction de milliseconde. Sur un tel paysage, une descente de gradient ferait l’affaire — le métaheuristique n’apporte rien, et c’est une bonne chose de le vérifier d’abord (baseline de validation).
Profit saisonnier (deuxième partie). On ajoute une cyclicité de marché :
P(x,y) = 50x + 80y − x² − 2y² − xy + 120·sin(1,2x)·sin(1,2y)
╰──────────────────────╯ ╰──────────────────────╯
profit de base (concave) saisonnalité du marché
Le terme sinusoïdal grave une grille d’une quinzaine de maxima locaux aux bassins profonds dans [0,20]². L’optimum n’est plus accessible analytiquement (∇P = 0 est un système non-linéaire sans solution fermée), et une descente de gradient resterait coincée dans le premier bassin rencontré. C’est ici que le métaheuristique devient indispensable.
Ce que montre la comparaison multi-seed (5 tirages). L’optimum global est à (x,y) ≈ (17,01 ; 16,99), P ≈ 1173,97. Mais un second bassin attracteur existe autour de (19,6 ; 14,4) (P ≈ 1170,8), et les deux algorithmes s’y laissent parfois piéger :
PSO (essaim, 30 particules)
SA (marche aléatoire recuite)
Meilleur profit sur 5 seeds
1173,97 (optimum global)
1173,97 (optimum global)
Moyenne sur 5 seeds
1173,34
1172,61
Atteint l’optimum global
4 seeds / 5
1 seed / 5
PSO est plus robuste sur ce paysage accidenté : son essaim de 30 particules explore plusieurs bassins en parallèle, ce qui lui permet de revenir du bassin sous-optimal vers le global dans la plupart des cas. SA, qui marche pas à pas depuis un seul point de départ, se laisse plus souvent enfermer dans un bassin local — sa capacité d’échappement (accepter un mouvement défavorable selon une probabilité qui décroît avec la température) ne suffit pas toujours quand le refroidissement est trop rapide.
À retenir. Aucun des deux algorithmes n’est infaillible : même PSO rate l’optimum global sur un seed (456 → 1170,81). C’est exactement pourquoi on exécute plusieurs seeds et qu’on rapporte meilleure + moyenne (et non un seul tirage, qui pourrait être chanceux ou malchanceux) — la même discipline que pour l’étude de la taille de population plus haut dans ce notebook. Sur un paysage unimodal, cette précaution est superflue (l’optimum analytique tranche) ; sur un paysage multimodal, elle devient la condition même d’une conclusion fiable. C’est tout l’intérêt d’un métaheuristique : non pas remplacer l’analyse quand elle suffit, mais prendre le relais là où elle échoue.
9. Exemple guide : recuit simule pour le TSP
Le Voyageur de Commerce (TSP) consiste à trouver le tour le plus court visitant toutes les villes une fois. C’est un problème d’optimisation combinatoire (l’espace est l’ensemble des permutations). Le recuit simulé est bien adapté : le voisinage = échange de deux villes dans le tour.
// TSP par recuit simule (solution = permutation des villes, voisinage = 2-opt : inversion de segment).// 15 villes aleatoires (seed fixee pour reproductibilite).var rngTsp =newRandom(42);int nVilles =15;var villes =new(double x,double y)[nVilles];for(int i =0; i < nVilles; i++) villes[i]=(rngTsp.NextDouble()*100, rngTsp.NextDouble()*100);staticdoubleTspDistance(int[] tour,(double x,double y)[] v){double d =0;for(int i =0; i < tour.Length; i++){var a = v[tour[i]];var b = v[tour[(i+1)% tour.Length]]; d += Math.Sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));}return d;}// Tour initial aleatoire (Fisher-Yates)int[] cur = Enumerable.Range(0, nVilles).ToArray();for(int i = nVilles -1; i >0; i--){int j = rngTsp.Next(i+1);(cur[i], cur[j])=(cur[j], cur[i]);}double initD =TspDistance(cur, villes);// distance du tour aleatoire initial (reference)int[] bestTsp =(int[])cur.Clone();double curD = initD, bestD = initD;double T =50.0;for(int it =0; it <40000; it++){// Voisin 2-opt : inverser le segment entre deux indices i1 < i2int i1 = rngTsp.Next(nVilles), i2 = rngTsp.Next(nVilles);if(i1 > i2)(i1, i2)=(i2, i1);int[] cand =(int[])cur.Clone(); Array.Reverse(cand, i1, i2 - i1 +1);double candD =TspDistance(cand, villes);double delta = candD - curD;if(delta <0|| rngTsp.NextDouble()< Math.Exp(-delta / T)){ cur = cand; curD = candD;if(curD < bestD){ bestD = curD; bestTsp =(int[])cur.Clone();}} T *=0.9997;if(T <1e-9) T =1e-9;}Console.WriteLine($"TSP ({nVilles} villes) par recuit simule (voisinage 2-opt) :");Console.WriteLine($" Tour aleatoire initial : {initD:F2}");Console.WriteLine($" Tour optimise (SA) : {bestD:F2}");Console.WriteLine($" Amelioration : {initD/bestD:F2}x");Console.WriteLine($" Permutation optimale : [{string.Join(",", bestTsp.Take(8))}, ...]");Console.WriteLine();Console.WriteLine("Le voisinage 2-opt (inversion de segment) est le standard pour le TSP : il remodele localement");Console.WriteLine("le tour pour eliminer les arenes croisees, la ou un simple swap de 2 villes est trop faible.");
TSP (15 villes) par recuit simule (voisinage 2-opt) :
Tour aleatoire initial : 548,69
Tour optimise (SA) : 284,64
Amelioration : 1,93x
Permutation optimale : [9,12,3,6,0,8,10,7, ...]
Le voisinage 2-opt (inversion de segment) est le standard pour le TSP : il remodele localement
le tour pour eliminer les arenes croisees, la ou un simple swap de 2 villes est trop faible.
Lecture du TSP : 548,69 vers 284,64, et pourquoi 2-opt est le bon voisinage
Le recuit part d’un tour aléatoire (548,69) et le resserre à 284,64 (amélioration 1,93×). Le point structurel : sur un problème combinatoire (l’espace = permutations de 15 villes), le voisinage définit l’algorithme. Un simple swap de deux villes casse quatre arêtes et en recrée quatre — quasi-tous les voisins sont mauvais. L’inversion de segment (2-opt) retire deux arêtes croisées et les rebranche droites : dans le plan euclidien, décroiser raccourcit toujours (inégalité triangulaire), donc une grande fraction des voisins 2-opt sont améliorants. Le recuit n’a plus qu’à enchaîner les décroisements et à accepter quelques détours — d’où l’efficacité.
10. Tranche 2 : la même chose avec une librairie SOTA
Les sections précédentes ont construit PSO, SA et GA from scratch pour en exposer la mécanique interne. Dans la pratique, on s’appuie rarement sur du code maison : on utilise une librairie éprouvée, exactement comme le jumeau Python de ce notebook le fait avec MEALPy. Côté .NET, l’équivalent de référence pour les algorithmes évolutifs est GeneticSharp — la librairie C# open-source la plus utilisée pour l’évolutionnaire, et d’ailleurs le socle sur lequel notre MetaGeneticSharp du cours avancé (Partie 4) est construite.
Cette tranche 2 reprend les quatre mêmes fonctions de benchmark que le §6 (Sphere, Rastrigin, Rosenbrock, Ackley ; dim=10, 200 générations) et les résout avec GeneticSharp, puis compare objectif final et temps de calcul avec notre GA maison. C’est la parité lib-vs-lib : le GA from-scratch d’un côté, la librairie SOTA de l’autre, sur les mêmes instances.
// Tranche 2 : GeneticSharp (librairie SOTA C#) sur les 4 fonctions du benchmark (memes instances qu'au §6).// dim=10, 50 individus, 200 generations, seed 42 -> directement comparable au GA from-scratch.#r "nuget: GeneticSharp"using GeneticSharp;// GeneticSharp expose son generateur aleatoire via IRandomization (branchable). On branche une// version sempable (seed 42) pour un resultat reproductible, comme le GA from-scratch.publicclass SeededRandomization : IRandomization{privatereadonly Random _rng =newRandom(42);publicintGetInt(int min,int max)=> _rng.Next(min, max);publicint[]GetInts(int length,int min,int max){var a =newint[length];for(int i =0; i < length; i++) a[i]= _rng.Next(min, max);return a;}publicint[]GetUniqueInts(int length,int min,int max){var pool =new List<int>();for(int v = min; v < max; v++) pool.Add(v);var a =newint[length];for(int i =0; i < length && pool.Count>0; i++){int idx = _rng.Next(pool.Count); a[i]= pool[idx]; pool.RemoveAt(idx);}return a;}publicfloatGetFloat()=>(float)_rng.NextDouble();publicfloatGetFloat(float min,float max)=> min +(float)_rng.NextDouble()*(max - min);publicdoubleGetDouble()=> _rng.NextDouble();publicdoubleGetDouble(double min,double max)=> min + _rng.NextDouble()*(max - min);}RandomizationProvider.Current=newSeededRandomization();// Fitness de minimisation generique : GeneticSharp MAXIMISE la fitness -> on renvoie -f.publicclass MinFitness : IFitness{public Func<double[],double> F;publicMinFitness(Func<double[],double> f){ F = f;}publicdoubleEvaluate(IChromosome c)=>-F(((FloatingPointChromosome)c).ToFloatingPoints());}// Run GeneticSharp sur une fonction : chromosome binaire 10 genes (64 bits/gene, 10 fractionnels),// croisement uniforme (gene par gene) + mutation flip-bit (le classique des GA binaires de Holland/Goldberg).doubleRunGS(string name, Func<double[],double> f,double lb,double ub){int[] bits = Enumerable.Repeat(64,10).ToArray();int[] fr = Enumerable.Repeat(10,10).ToArray();double[] mn = Enumerable.Repeat(lb,10).ToArray();double[] mx = Enumerable.Repeat(ub,10).ToArray();var pop =newPopulation(50,50,newFloatingPointChromosome(mn, mx, bits, fr));var ga =newGeneticAlgorithm(pop,newMinFitness(f),newTournamentSelection(3),newUniformCrossover(),newFlipBitMutation()); ga.Termination=newGenerationNumberTermination(200); ga.CrossoverProbability=0.9f; ga.MutationProbability=0.5f;var sw = System.Diagnostics.Stopwatch.StartNew(); ga.Start(); sw.Stop();double obj =-ga.BestChromosome.Fitness.Value; Console.WriteLine($" {name,-12} {obj,-14:F4} {sw.Elapsed.TotalMilliseconds,-8:F0} ms");return obj;}Console.WriteLine("GeneticSharp (UniformCrossover + FlipBit, dim=10, 50 ind x 200 gen, seed 42) :");Console.WriteLine($" {"Fonction",-12} {"Objectif",-14} {"Temps",-8}");Console.WriteLine(newstring('-',38));double gsSphere =RunGS("Sphere", Sphere,-10.0,10.0);double gsRastrigin =RunGS("Rastrigin", Rastrigin,-5.12,5.12);double gsRosenbrock =RunGS("Rosenbrock", Rosenbrock,-5.0,5.0);double gsAckley =RunGS("Ackley", Ackley,-32.0,32.0);
Installed Packages
GeneticSharp, 3.1.4
GeneticSharp (UniformCrossover + FlipBit, dim=10, 50 ind x 200 gen, seed 42) :
Fonction Objectif Temps
--------------------------------------
Sphere 0,0000 2174 ms
Rastrigin 58,2798 1565 ms
Rosenbrock 560,6489 536 ms
Ackley 19,9668 535 ms
// Comparaison lib-vs-lib : GA from-scratch (§5) vs GeneticSharp (Tranche 2) sur les 4 fonctions.// On relance le GA from-scratch (seed 42, meme budget) pour un comparatif cote-a-cote.doubleScratchGA(Func<double[],double> f,double lb,double ub)=>newGA(dim:10, lb: lb, ub: ub, f: f, popSize:50, epochs:200, seed:42).Solve().fitness;Console.WriteLine("Comparaison lib-vs-lib sur le benchmark (dim=10, 200 generations, seed 42) :");Console.WriteLine($" {"Fonction",-12} {"GA from-scratch",-18} {"GeneticSharp",-18}");Console.WriteLine(newstring('-',50));Console.WriteLine($" {"Sphere",-12} {ScratchGA(Sphere, -10.0, 10.0),-18:F4} {gsSphere,-18:F4}");Console.WriteLine($" {"Rastrigin",-12} {ScratchGA(Rastrigin, -5.12, 5.12),-18:F4} {gsRastrigin,-18:F4}");Console.WriteLine($" {"Rosenbrock",-12} {ScratchGA(Rosenbrock, -5.0, 5.0),-18:F4} {gsRosenbrock,-18:F4}");Console.WriteLine($" {"Ackley",-12} {ScratchGA(Ackley, -32.0, 32.0),-18:F4} {gsAckley,-18:F4}");Console.WriteLine();Console.WriteLine("Les deux GA retombent vers f=0 sur Sphere (convexe, facile). Sur les fonctions plus");Console.WriteLine("difficiles, l'ecart reflege le codage : le from-scratch (genes continus, croisement");Console.WriteLine("arithmetique, mutation gaussienne) vs GeneticSharp (genes binaires, flip-bit) -- deux");Console.WriteLine("designs de GA legitimes, chacun avec ses forces selon le paysage.");
Comparaison lib-vs-lib sur le benchmark (dim=10, 200 generations, seed 42) :
Fonction GA from-scratch GeneticSharp
--------------------------------------------------
Sphere 0,0002 0,0000
Rastrigin 8,5265 58,2798
Rosenbrock 8,0688 560,6489
Ackley 6,3691 19,9668
Les deux GA retombent vers f=0 sur Sphere (convexe, facile). Sur les fonctions plus
difficiles, l'ecart reflege le codage : le from-scratch (genes continus, croisement
arithmetique, mutation gaussienne) vs GeneticSharp (genes binaires, flip-bit) -- deux
designs de GA legitimes, chacun avec ses forces selon le paysage.
Lecture : from-scratch vs librairie SOTA
Les deux approches attaquent les quatre mêmes instances (Sphere, Rastrigin, Rosenbrock, Ackley ; dim=10, 200 générations, graine 42) avec un budget comparable, mais par des voies différentes :
le GA from-scratch (§5) code chaque gène en continu, avec croisement arithmétique (moyenne des parents) et mutation gaussienne ;
GeneticSharp code chaque gène en binaire (64 bits, dont 10 fractionnels), avec croisement uniforme (gène par gène) et mutation flip-bit — le classique des GA binaires fondateurs (Holland, Goldberg).
Sphere (convexe, optimum trivial à l’origine) sert de sanity check : les deux GA y retombent vers f ≈ 0, ce qui valide le branchement de la librairie. Sur les fonctions plus difficiles, l’écart entre les deux colonnes mesure l’effet du codage et des opérateurs à budget égal : le codage continu du from-scratch est naturellement à l’aise sur les vallées étroites (Rosenbrock) et les plateaux (Ackley), tandis que le codage binaire de GeneticSharp y est plus rigide — sans que cela ne le disqualifie (c’est l’encodage canonique des GA historiques, performant sur d’autres familles de problèmes, notamment combinatoires). C’est précisément l’intérêt du pont lib-vs-lib : confronter notre réimplémentation à un point de référence reproductible, fourni par une librairie reconnue, sans avoir à coder les opérateurs soi-même.
Parité avec le jumeau Python. Le notebook Python branche MEALPy (PSO, ABC, SA, BRO…) exactement pour la même raison. La Tranche 2 fait pour .NET ce que MEALPy fait pour Python : appeler une librairie de métaheuristiques reconnue plutôt qu’une réimplémentation maison. GeneticSharp est d’ailleurs le socle sur lequel notre MetaGeneticSharp (cours avancé, Partie 4) est construite.
11. Exercices
Trois exercices pour approfondir : ABC (4e métaheuristique majeure), schedules d’inertie PSO, et une 5e fonction de benchmark (Schwefel, fameuse pour son optimum global loin du centre).
Attendus — les trois exercices
ABC (exercice 1). Trois phases attendues : employées (exploitation locale autour des sources), onlookers (choix pondéré par la fitness), éclaireuses (abandon des sources épuisées après limit tours sans progrès — c’est l’échappement aux optima locaux). Résultat typique sur Rastrigin : entre PSO et SA ; l’abandon doit parfois se déclencher pour débloquer un run bloqué à ~6. Schedules d’inertie (exercice 2). w linéaire 0,9 → 0,4 est le réglage de référence ; w constant haut = exploration qui ne converge jamais, w constant bas = convergence prématurée — attendez-vous à voir la moyenne multi-seed se dégrader aux deux extrémités, optimum au milieu. Schwefel (exercice 3). L’optimum est à ~420,97 par dimension, au bord du domaine [-500, 500] — un paysage qui piège les méthodes trop centrées : PSO avec initialisation uniforme s’en sort, GA avec croisement arithmétique tire la population vers le centre et échoue. Le contraste avec Rosenbrock (même phénoménologie de centrage, direction opposée) est la finalité du défi.
// Exercice 1 : Artificial Bee Colony (ABC).// ABC modelise le butinage des abeilles : employees (exploitent une source),// onlookers (choisissent selon la qualite), scouts (explorent de nouvelles sources).publicclass ABC{// TODO etudiant : implementez ABC (employe/onlooker/scout phases)// Indice : phase employe -> voisin d'une source aleatoire ; phase onlooker -> sélection proportionnelle ;// phase scout -> remplacer les sources epuisees (limit sans amelioration) par une source aleatoire.publicdouble[] Best;publicdouble BestFitness =double.MaxValue;public(double[] sol,double fit)Solve(){return(newdouble[0],0);// TODO etudiant}}Console.WriteLine("Exercice 1 a completer : implementez ABC et comparez-le a PSO sur Rastrigin.");
Exercice 1 a completer : implementez ABC et comparez-le a PSO sur Rastrigin.
Transition vers l’exercice 2. Le schedule d’inertie est le levier qui gère exactement la tension mesurée au §7 : w haut préserve la diversité (exploration), w bas la concentre (exploitation). Votre protocole : les mêmes 8 seeds que la cellule multi-seed, un schedule par run — c’est la seule façon d’attribuer l’effet au schedule et non au hasard.
// Exercice 2 : Comparer différents schedules d'inertie pour PSO.// Le schedule par defaut est lineaire decroissant (wMax=0.9 -> wMin=0.4).// TODO etudiant : testez (a) inertie constante w=0.7, (b) decroissance exponentielle w=wMax*exp(-3*it/epochs)// et comparez la fitness finale sur Rastrigin.double fitConstant =0.0;// TODO etudiant : PSO avec w fixedouble fitExponential =0.0;// TODO etudiant : PSO avec w exponentielConsole.WriteLine($"Exercice 2 a completer : fitness w=lineaire(?) vs w=constante({fitConstant}) vs w=exponentiel({fitExponential}).");
Exercice 2 a completer : fitness w=lineaire(?) vs w=constante(0) vs w=exponentiel(0).
// Exercice 3 : Ajouter la fonction de Schwefel et l'optimiser.// Schwefel : f(x) = 418.9829*n - sum(x_i * sin(sqrt(|x_i|))), x in [-500, 500].// Particularite : l'optimum global est loin du centre (x_i = 420.9687), defie les algorithmes// qui convergent vers le centre (comme beaucoup de variants PSO mal reglees).staticdoubleSchwefel(double[] x){double s =0;int n = x.Length;foreach(double v in x) s += v * Math.Sin(Math.Sqrt(Math.Abs(v)));return418.9829*n - s;// TODO etudiant : verifier l'optimum f=0 a x_i=420.9687}double fitSchwefel =0.0;// TODO etudiant : optimiser Schwefel dim=10 avec PSO, bornes [-500,500]Console.WriteLine($"Exercice 3 a completer : Schwefel(420.9687,...)={Schwefel(Enumerable.Repeat(420.9687,5).ToArray()):F4} (attendu ~0), optimiser en dim=10 : {fitSchwefel} (a completer).");
Exercice 3 a completer : Schwefel(420.9687,...)=0,0001 (attendu ~0), optimiser en dim=10 : 0 (a completer).
Conclusion et resume
Ce jumeau C# a reimplemente from scratch (coeur BCL pur, 0 NuGet) les métaheuristiques majeures du notebook Python mealpy, puis les a confrontées à la librairie SOTA GeneticSharp (Tranche 2, §10) :
4 fonctions de benchmark (Sphere, Rastrigin, Rosenbrock, Ackley) avec optima verifies
PSO (Particle Swarm Optimization) : inertie + cognitif + social
SA (Simulated Annealing) : acceptation de Metropolis, refroidissement geometrique
GA (Genetic Algorithm) : tournoi, croisement arithmetique, mutation gaussienne, elitisme
Tranche 2 / GeneticSharp : pont lib-vs-lib sur Rosenbrock (§10) — la librairie SOTA sur la même instance que le from-scratch
Benchmark comparatif PSO/SA/GA sur les 4 fonctions
Analyse de paramètres (taille de population)
2 exemples guides : optimisation de profit (retrouve l’optimum analytique x≈17.14, y≈15.71), TSP par recuit simule
Parite avec le notebook Python
Quantite
Python (mealpy)
C# (BCL)
Optimum Sphere/Rastrigin/Ackley
[0,…,0], f=0
idem
Optimum Rosenbrock
[1,…,1], f=0
idem
PSO params (w,c1,c2)
0.9, 2.0, 2.0
idem
Optimum profit (analytique)
x=17.14, y=15.71, P=1057.1
idem
Tendance : PSO fort sur multimodal
oui
oui
Les valeurs numériques exactes (fitness finale, temps) différent C# vs Python : mealpy utilise des heuristiques additionnelles (clamping, restart) et Mersenne Twister vs System.Random ici. Les tendances (PSO excelle sur multimodal, GA robuste sur vallee, SA sur plateaux) et les optima retrouves sont identiques.
Prochaines étapes
Search-03-Informed (A*) : recherche informe avec heuristiques (cas ou l’optimalite est garantie, contrairement aux métaheuristiques).