Suite de la tranche 1 (recuit simulé). Cette tranche 2 présente le Particle Swarm Optimization (PSO) — une métaheuristique d’essaim (population-based) radicalement différente du recuit simulé (trajectoire unique). Chaque particule communique sa meilleure trouvaille aux autres : l’intelligence collective émerge d’interactions locales.
Complémentarité (.NET from-scratch ↔︎ Python/MEALPy), pas workaround (#3801)
comprendre la dynamique vitesse/inertie de l’intérieur
Pas d’équivalent NuGet maintenu et didactique à MEALPy en .NET. Le twin .NET démontre PSO à la main — l’occasion pédagogique (cf tranche 1 SA, GameTheory-2 Nash).
Objectifs d’apprentissage (tranche 2)
À la fin de cette tranche, vous saurez : 1. Distinguer optimisation trajectoire unique (SA) vs essaim (PSO) 2. Implémenter la mise à jour de vitesse PSO (inertie + cognitif + social) 3. Comprendre le rôle des coefficients\(w\), \(c_1\), \(c_2\) (inertie, confiance personnelle, confiance sociale) 4. Vérifier PSO sur un benchmark multimodal non-convexe (Rastrigin) 5. Étudier la convergence d’un essaim (best global vs itérations)
Note : Le PSO (Kennedy & Eberhart, 1995) s’inspire des bancs de poissons et nuées d’oiseaux. Aucune particule n’est intelligente seule, mais l’essaim converge collectivement vers les bonnes régions en partageant l’information. C’est un modèle d’intelligence en essaim (swarm intelligence), à l’opposé de la descente individuelle.
Le recuit simulé (tranche 1) maintient une seule solution qu’il perturbe itérativement. Le PSO maintient un essaim de \(N\) particules, chacune avec une position\(x_i\) et une vitesse\(v_i\). Les particules se souviennent de leur meilleure position personnelle (\(p_i\), personal best) et partagent la meilleure position globale (\(g\), global best).
Aspect
Recuit simulé (SA)
PSO
Solutions vivantes
1 (trajectoire)
\(N\) (essaim)
Mémoire
meilleure globale
personnelle et globale
Communication
aucune
globale (best partagé)
Acceptation dégradation
probabiliste (Metropolis)
continue (toujours, via vitesse)
Le PSO n’a pas de critère d’acceptation probabiliste : il déplace toutes ses particules à chaque itération, guidées par \(p_i\) et \(g\).
\[f(x) = 10n + \sum_i \left( x_i^2 - 10\cos(2\pi x_i) \right), \quad x^* = \mathbf{0}, \quad f(x^*) = 0\]Multimodal très raboteux (beaucoup d’optima locaux), un seul minimum global en \(\mathbf{0}\). Test discriminant : PSO doit échapper aux optima locaux grâce à l’exploration collective.
Pourquoi ce duo ? Sphere est le témoin unimodal : un seul bassin d’attraction, aucune structure à exploiter — il valide la mécanique. Rastrigin est le terrain de test : une trame régulière de minima locaux (un par pas entier dans chaque dimension) superposée à une parabole — il punit toute méthode qui traite un minimum local comme une destination.
// Fonctions de benchmark (self-contained, cf. tranche 1).staticdoubleSphere(double[] x)=> x.Select(xi => xi * xi).Sum();staticdoubleRastrigin(double[] x){int n = x.Length;double s =0;for(int i =0; i < n; i++) s += x[i]* x[i]-10.0* Math.Cos(2* Math.PI* x[i]);return10.0* n + s;}// Domaine de recherche ([-5.12, 5.12]^n recommandé pour Rastrigin).constdouble LO =-5.12, HI =5.12;// Vérification : minimum global connu = 0 en l'origine.var origin =newdouble[]{0.0,0.0};var sb =newStringBuilder();sb.AppendLine($"Sphere(0,0) = {Sphere(origin):F6} (attendu 0)");sb.AppendLine($"Rastrigin(0,0) = {Rastrigin(origin):F6} (attendu 0)");var rough =newdouble[]{3.0,-2.0};sb.AppendLine($"Rastrigin(3,-2) = {Rastrigin(rough):F6} (point raboteux hors optimum)");sb.ToString().Display();
où : - \(w\) : inertie (poids de la vitesse précédente — garde l’élan), - \(c_1\) : coefficient cognitif (confiance en sa propre expérience \(p_i\)), - \(c_2\) : coefficient social (confiance dans l’essaim \(g\)), - \(r_1, r_2 \sim \mathcal{U}(0,1)\) : tirages aléatoires (stochastique).
Interprétation des trois termes
Inertie : la particule continue dans sa lancée (évite les oscillations brutales).
Cognitif : tire vers le meilleur endroit déjà visité par cette particule (exploitation locale).
Social : tire vers le meilleur endroit connu de l’essaim (exploitation collective).
Équilibre exploration/exploitation
Un \(w\)élevé favorise l’exploration (la vitesse domine, l’essaim balaye large). Un \(w\)faible favorise l’exploitation (les termes cognitif/social dominent, convergence rapide). Souvent on décline\(w\) linéairement (élevé au début, faible à la fin) — schéma d’inertie adaptatif.
4. Implémentation from-scratch du PSO
L’implémentation maintient un tableau de particules (position, vitesse, personal best). À chaque itération : mise à jour des vitesses, déplacement, évaluation, mise à jour de \(p_i\) et \(g\).
// Particle Swarm Optimization (PSO) from-scratch.// f : fonction objectif a minimiser// dim : dimension// nParts : taille de l'essaim (nombre de particules)// iters : nombre d'iterations// w : coefficient d'inertie// c1 : coefficient cognitif (confiance en p_i)// c2 : coefficient social (confiance en g)// rng : PRNG partage (seed fixe hors de la fonction)static(double[] gbest,double fbest, List<double> history)PSO( Func<double[],double> f,int dim,int nParts,int iters,double w,double c1,double c2, Random rng){// Initialisation : positions et vitesses aleatoires dans le domaine.double[][] pos =newdouble[nParts][];double[][] vel =newdouble[nParts][];double[][] pbest =newdouble[nParts][];double[] fpbest =newdouble[nParts];for(int i =0; i < nParts; i++){ pos[i]=newdouble[dim]; vel[i]=newdouble[dim];for(int d =0; d < dim; d++){ pos[i][d]= LO + rng.NextDouble()*(HI - LO); vel[i][d]=(rng.NextDouble()-0.5)*(HI - LO)*0.5;// vitesse initiale moderee} pbest[i]=(double[])pos[i].Clone(); fpbest[i]=f(pos[i]);}// Global best initial.int gidx =0;for(int i =1; i < nParts; i++)if(fpbest[i]< fpbest[gidx]) gidx = i;double[] gbest =(double[])pbest[gidx].Clone();double fbest = fpbest[gidx];var history =new List<double>{ fbest };for(int it =0; it < iters; it++){for(int i =0; i < nParts; i++){for(int d =0; d < dim; d++){double r1 = rng.NextDouble();double r2 = rng.NextDouble();// Mise a jour vitesse (inertie + cognitif + social). vel[i][d]= w * vel[i][d]+ c1 * r1 *(pbest[i][d]- pos[i][d])+ c2 * r2 *(gbest[d]- pos[i][d]);// Deplacement. pos[i][d]+= vel[i][d];// Bornage (rebond doux : on ramene dans le domaine).if(pos[i][d]< LO){ pos[i][d]= LO; vel[i][d]*=-0.5;}if(pos[i][d]> HI){ pos[i][d]= HI; vel[i][d]*=-0.5;}}// Evaluation + mise a jour personal best.double fi =f(pos[i]);if(fi < fpbest[i]){ fpbest[i]= fi; pbest[i]=(double[])pos[i].Clone();}// Mise a jour global best.if(fi < fbest){ fbest = fi; gbest =(double[])pos[i].Clone();}} history.Add(fbest);}return(gbest, fbest, history);}"PSO prêt (inertie w + cognitif c1 + social c2, bornage par rebond doux).".Display();
PSO prêt (inertie w + cognitif c1 + social c2, bornage par rebond doux).
// PSO sur Sphere (unimodal) : sanity check, doit approcher 0.var rng =newRandom(42);var(gbest_sph, fbest_sph, hist_sph)=PSO(Sphere, dim:2, nParts:30, iters:100, w:0.7, c1:1.5, c2:1.5, rng);var sb =newStringBuilder();sb.AppendLine("=== PSO sur Sphere (unimodal) ===");sb.AppendLine($"Meilleur x trouvé : ({FI(gbest_sph[0])}, {FI(gbest_sph[1])})");sb.AppendLine($"Meilleur f(x) : {FI(fbest_sph)} (optimum global = 0)");sb.AppendLine($"Distance à 0 : {FI(Math.Sqrt(gbest_sph[0]*gbest_sph[0]+gbest_sph[1]*gbest_sph[1]))}");sb.AppendLine($"Itérations : {hist_sph.Count - 1}");sb.ToString().Display();
=== PSO sur Sphere (unimodal) ===
Meilleur x trouvé : (-0.0000, 0.0000)
Meilleur f(x) : 0.0000 (optimum global = 0)
Distance à 0 : 0.0000
Itérations : 100
Interprétation : Sphere
Sur Sphere (convexe, un seul minimum), PSO converge très vite vers l’origine : l’essaim entier est attiré par le puits de potentiel. C’est le sanity check — l’essaim ne peut pas échouer sur un problème unimodal. L’intérêt du PSO se révèle sur les paysages multimodaux.
Lecture du résultat (sortie commitée ci-dessus). L’essaim atteint Meilleur f(x) = 0.0000 en 100 itérations, Distance à 0 : 0.0000 : convergence exacte à la précision d’affichage. Sur ce paysage convexe, chaque composante de la mise à jour de vitesse agit comme une descente approximative — la mémoire cognitive (\(pbest\)) tire la particule vers son meilleur point visité, la mémoire sociale (\(gbest\)) propage le meilleur global, et aucun piège local ne peut intercepter cette double attraction.
Un échec ici ne signale donc jamais une limite de l’algorithme mais un bug d’implémentation (bornes de vitesse, mise à jour des mémoires). Noter aussi ce que l’essaim n’apporte PAS : sur un terrain convexe, le recuit simulé de la tranche 1 fait aussi bien avec une seule solution.
// PSO sur Rastrigin (multimodal) : le vrai test.var rng =newRandom(42);var(gbest_ras, fbest_ras, hist_ras)=PSO(Rastrigin, dim:2, nParts:30, iters:200, w:0.7, c1:1.5, c2:1.5, rng);var sb =newStringBuilder();sb.AppendLine("=== PSO sur Rastrigin (multimodal, non-convexe) ===");sb.AppendLine($"Meilleur x trouvé : ({FI(gbest_ras[0])}, {FI(gbest_ras[1])})");sb.AppendLine($"Meilleur f(x) : {FI(fbest_ras)} (optimum global = 0)");sb.AppendLine($"Distance à 0 : {FI(Math.Sqrt(gbest_ras[0]*gbest_ras[0]+gbest_ras[1]*gbest_ras[1]))}");sb.AppendLine();sb.AppendLine("Convergence (best global tous les 40 itérations) :");for(int i =0; i < hist_ras.Count; i +=40) sb.AppendLine($" itération {i,3} : best f = {FI(hist_ras[i])}");sb.ToString().Display();
=== PSO sur Rastrigin (multimodal, non-convexe) ===
Meilleur x trouvé : (-0.0000, 0.0000)
Meilleur f(x) : 0.0000 (optimum global = 0)
Distance à 0 : 0.0000
Convergence (best global tous les 40 itérations) :
itération 0 : best f = 8.5486
itération 40 : best f = 0.0000
itération 80 : best f = 0.0000
itération 120 : best f = 0.0000
itération 160 : best f = 0.0000
itération 200 : best f = 0.0000
Interprétation : Rastrigin — PSO vs descente pure
Rastrigin est parsemé d’optima locaux (la partie \(-10\cos(2\pi x_i)\) crée des oscillations). Une descente pure s’y piège ; le recuit simulé (tranche 1) s’en sort par acceptation probabiliste de dégradations.
Le PSO s’en sort autrement : grâce à l’essaim, si une particule découvre un meilleur bassin, l’information se propage (terme social \(c_2 r_2 (g - x_i)\) tire tout l’essaim vers \(g\)). C’est l’intelligence collective : la diversité de l’essaim explore plusieurs bassins simultanément, et le partage d’information focalise progressivement tout le monde sur le meilleur.
Sensibilité : si \(w\) est trop élevé, l’essaim diverge (les vitesses explosent). Si \(c_2\) domine trop tôt, l’essaim converge prématurément vers un optimum local (toutes les particules se ruent sur le premier \(g\) trouvé). L’équilibre \(w, c_1, c_2\) est crucial.
Lecture du résultat (sortie commitée ci-dessus). La trace mesure best f = 8.5486 à l’itération 0, puis 0.0000 dès l’itération 40, valeur verrouillée à 80, 120 et 160 : l’essaim n’approche pas l’optimum, il le capture et s’y maintient. Budget correspondant : 40 itérations × 30 particules = 1 200 évaluations de fonction (paramètres de l’appel : dim: 2, nParts: 30, w: 0.7, c1: c2: 1.5, graine 42).
Comparé au recuit simulé, même verdict mais mécanisme distinct : acceptation probabiliste de remontée d’un côté, partage d’information social de l’autre — le PSO achète la robustesse multimodale avec de la population, sans schedule de température à régler.
Une amélioration classique : faire décliner\(w\) linéairement de \(w_{\max}\) à \(w_{\min}\) sur les itérations. Au début (\(w\) élevé) : exploration large. À la fin (\(w\) faible) : exploitation fine. C’est l’équivalent PSO du schéma de température du recuit simulé.
Lue mécaniquement, l’inertie \(w\) joue le rôle d’un frottement contrôlé : haute, elle laisse la vitesse s’accumuler (grands pas, exploration) ; basse, elle amortit (petits pas, raffinement). La garder fixe contraint l’essaim à un seul régime ; la décrémenter linéairement lui fait changer de régime en cours de vol — sans la probabilité d’acceptation de remontée qui fait le travail côté recuit.
// Variante PSO avec inertie declinee lineairement (w_max -> w_min sur les iterations).static(double[] gbest,double fbest, List<double> history)PSODeclining( Func<double[],double> f,int dim,int nParts,int iters,double wMax,double wMin,double c1,double c2, Random rng){double[][] pos =newdouble[nParts][];double[][] vel =newdouble[nParts][];double[][] pbest =newdouble[nParts][];double[] fpbest =newdouble[nParts];for(int i =0; i < nParts; i++){ pos[i]=newdouble[dim]; vel[i]=newdouble[dim];for(int d =0; d < dim; d++){ pos[i][d]= LO + rng.NextDouble()*(HI - LO); vel[i][d]=(rng.NextDouble()-0.5)*(HI - LO)*0.5;} pbest[i]=(double[])pos[i].Clone(); fpbest[i]=f(pos[i]);}int gidx =0;for(int i =1; i < nParts; i++)if(fpbest[i]< fpbest[gidx]) gidx = i;double[] gbest =(double[])pbest[gidx].Clone();double fbest = fpbest[gidx];var history =new List<double>{ fbest };for(int it =0; it < iters; it++){double w = wMax -(wMax - wMin)* it / Math.Max(1, iters -1);// decline lineairefor(int i =0; i < nParts; i++){for(int d =0; d < dim; d++){double r1 = rng.NextDouble(), r2 = rng.NextDouble(); vel[i][d]= w * vel[i][d]+ c1 * r1 *(pbest[i][d]- pos[i][d])+ c2 * r2 *(gbest[d]- pos[i][d]); pos[i][d]+= vel[i][d];if(pos[i][d]< LO){ pos[i][d]= LO; vel[i][d]*=-0.5;}if(pos[i][d]> HI){ pos[i][d]= HI; vel[i][d]*=-0.5;}}double fi =f(pos[i]);if(fi < fpbest[i]){ fpbest[i]= fi; pbest[i]=(double[])pos[i].Clone();}if(fi < fbest){ fbest = fi; gbest =(double[])pos[i].Clone();}} history.Add(fbest);}return(gbest, fbest, history);}// Comparaison sur Rastrigin : inertie fixe (w=0.7) vs declinee (0.9 -> 0.4).var sb =newStringBuilder();sb.AppendLine("=== Comparaison inertie fixe vs déclinante sur Rastrigin (30 particules, 200 iters) ===");var rng1 =newRandom(42);var(_, fFixed, _)=PSO(Rastrigin,2,30,200, w:0.7, c1:1.5, c2:1.5, rng1);var rng2 =newRandom(42);var(_, fDecl, _)=PSODeclining(Rastrigin,2,30,200, wMax:0.9, wMin:0.4, c1:1.5, c2:1.5, rng2);sb.AppendLine($"Inertie fixe (w=0.7) : best f = {FI(fFixed)}");sb.AppendLine($"Inertie déclinante (0.9->0.4) : best f = {FI(fDecl)}");sb.AppendLine();sb.AppendLine(">>> L'inertie déclinante explore large au début, raffine à la fin.");sb.ToString().Display();
=== Comparaison inertie fixe vs déclinante sur Rastrigin (30 particules, 200 iters) ===
Inertie fixe (w=0.7) : best f = 0.0000
Inertie déclinante (0.9->0.4) : best f = 0.0000
>>> L'inertie déclinante explore large au début, raffine à la fin.
Interprétation : schéma d’inertie
Le schéma déclinant commence par explorer (inertie 0.9 = vitesses préservées, l’essaim balaye large) puis termine en exploitant (inertie 0.4 = vitesses amorties, convergence fine vers le meilleur bassin). Sur Rastrigin, cela aide à échapper aux optima locaux en début de course puis à préciser la solution en fin.
Ce compromis exploration→exploitation est le même concept que le schéma de température du recuit simulé (tranche 1) — deux algorithmes différents, un même principe sous-jacent.
Lecture du résultat (sortie commitée ci-dessus). À 30 particules et 200 itérations sur Rastrigin, inertie fixe (w = 0.7) et inertie déclinante (0.9 -> 0.4) atteignent toutes deux best f = 0.0000 : à ce budget, l’égalité est le résultat attendu, pas une réfutation du schéma déclinant. Le régime discriminant est le budget court ou le paysage élargi (dimension supérieure) : en réduisant le budget d’un facteur 5, la phase haute garde une chance d’exploration là où l’inertie fixe moyenne fige l’essaim entre exploration et raffinement — une variante facile à brancher sur l’implémentation ci-dessus pour voir l’écart s’ouvrir. Les exercices qui suivent balaient les autres leviers (coefficients, taille d’essaim, dimension).
Exercice 1 : Coefficients cognitif vs social
PSO est sensible au ratio \(c_1/c_2\). Complétez le stub pour comparer sur Rastrigin : (a) \(c_1 = 2.5, c_2 = 0.5\) (fortement cognitif — chaque particule truste son \(p_i\)), (b) \(c_1 = 0.5, c_2 = 2.5\) (fortement social — l’essaim se rue sur \(g\)), (c) \(c_1 = c_2 = 1.5\) (équilibré). Lequel donne le meilleur \(f(x)\) final ? Lequel converge le plus vite ? Interprétez.
// Exercice 1 : comparaison coefficients cognitif/social (étudiant à compléter)// Indice 1 : reset rng = new Random(42) avant chaque run pour isoler l'effet de c1/c2.// Indice 2 : garder w=0.7 fixe, varier seulement (c1, c2) parmi (2.5,0.5), (0.5,2.5), (1.5,1.5).// TODO étudiant"Exercice 1 à compléter — comparer c1/c2 = (2.5,0.5) / (0.5,2.5) / (1.5,1.5) sur Rastrigin.".Display();
Exercice 1 à compléter — comparer c1/c2 = (2.5,0.5) / (0.5,2.5) / (1.5,1.5) sur Rastrigin.
Exercice 2 : Taille de l’essaim
Complétez le stub pour tester \(N \in \{10, 30, 100, 300\}\) particules sur Rastrigin (mêmes itérations, même seed). Un grand essaim explore-t-il mieux ? À partir de quel \(N\) le gain marginal devient-il négligeable ? Tracez le best final en fonction de \(N\).
// Exercice 2 : impact de la taille de l'essaim (étudiant à compléter)// Indice : boucler sur nParts in {10, 30, 100, 300}, reset rng avant chaque run, collecter fbest.// TODO étudiantint[] sizes ={10,30,100,300};"Exercice 2 à compléter — sweep taille d'essaim {10,30,100,300} sur Rastrigin.".Display();
Exercice 2 à compléter — sweep taille d'essaim {10,30,100,300} sur Rastrigin.
Exercice 3 : Dimension
Reprenez le sweep de la tranche 1 (exercice dimension) pour PSO : testez \(\mathrm{dim} \in \{2, 5, 10, 20\}\) sur Rastrigin à taille d’essaim fixe. PSO se dégrade-t-il plus ou moins vite que SA quand la dimension augmente ? C’est le phénomène de malédiction de la dimension — l’essaim a de plus en plus de mal à couvrir l’espace.
// Exercice 3 : impact de la dimension (étudiant à compléter)// Indice : boucler sur dim in {2,5,10,20}, reset rng avant chaque run, augmenter iters pour les grandes dims.// TODO étudiantint[] dims ={2,5,10,20};"Exercice 3 à compléter — sweep dimension {2,5,10,20} sur Rastrigin (comparer dégradation vs SA tranche 1).".Display();
Exercice 3 à compléter — sweep dimension {2,5,10,20} sur Rastrigin (comparer dégradation vs SA tranche 1).
6. Conclusion (tranche 2)
Cette tranche 2 a présenté le Particle Swarm Optimization — la métaheuristique d’essaim, complémentaire du recuit simulé (trajectoire unique) de la tranche 1.
Complémentarité SA ↔︎ PSO : SA = trajectoire unique avec acceptation probabiliste de dégradations ; PSO = essaim avec intelligence collective (partage du best). Deux stratégies distinctes pour le même compromis exploration/exploitation.
Intelligence collective : aucune particule n’est intelligente seule, mais l’essaim converge collectivement en propageant l’information (terme social). C’est le principe du swarm intelligence.
Schéma d’inertie = schéma de température : faire décliner \(w\) (PSO) est l’équivalent conceptuel de faire chuter \(T\) (SA) — exploration initiale, exploitation finale.
Tranches suivantes (marathon #4956)
Tranche 3 : Artificial Bee Colony (ABC) — autre essaim (abeilles butineuses/employées/observatrices), sur Rosenbrock.
Tranche 4 : benchmark comparatif SA / PSO / ABC + analyse de sensibilité multi-paramètres.
Références
Kennedy, J., Eberhart, R. (1995). Particle Swarm Optimization. Proc. IEEE Int. Conf. Neural Networks.
Shi, Y., Eberhart, R. (1998). A Modified Particle Swarm Optimizer (inertie déclinante).