MGS-13 — Visualiser les paysages dé-biaisés : pourquoi la rotation casse la séparabilité
Le notebook MGS-12 a mesuré numériquement l’effet de la rotation sur les métaheuristiques : sur Rastrigin rotatée, l’algorithme génétique (GA) axis-aligned perd environ +2847 en fitness, tandis que le Differential Evolution (DE) n’en perd qu’environ +2. Ce notebook rend ce résultat visible — on dessine le paysage de fitness avant/après rotation et l’on voit pourquoi un optimiseur axe-par-axe échoue là où un optimiseur rotation-robuste tient.
Fil conducteur
Une fonction séparable vérifie \(f(x,y)=g(x)+h(y)\) : on peut optimiser chaque axe indépendamment. Sphere, Rastrigin, Rosenbrock sont séparables dans leurs axes canoniques.
Une rotation\(M\) (matrice orthogonale, \(M M^\top = I\)) mélange les coordonnées : \(f'(x,y)=f(M\cdot(x,y))\). Sauf si \(f\) est invariante par rotation (Sphere), des termes croisés apparaissent et la séparabilité disparaît.
C’est le principe des benchmarks CEC « rotated » : neutraliser les optimiseurs qui exploitent l’alignement des axes (coordinate descent, croisements axis-aligned).
On réutilise les décorateurs compositionnels du fork — ShiftedFitness (translate l’optimum hors du centre, neutralise le central-bias) et RotatedFitness (rotates les coordonnées par une matrice orthogonale, neutralise l’axis-alignment bias) — sans jamais réimplémenter les maths canoniques. Les deux se composent en la variante CEC complète : new RotatedFitness(new ShiftedFitness(inner, offset), M).
Voir aussi : MGS-8 (rendu de paysage de fitness), MGS-12 (mesure numérique axis-alignment).
// Fork DLLs loaded from the self-contained Extensions output (one-stop dir: it also ships// System.Drawing.Common.dll AND SkiaSharp.dll, needed at runtime by the graphic landscape types).#r "..\MetaGeneticSharp\src\MetaGeneticSharp.Extensions\bin\Debug\net9.0\GeneticSharp.Infrastructure.Framework.dll"#r "..\MetaGeneticSharp\src\MetaGeneticSharp.Extensions\bin\Debug\net9.0\GeneticSharp.Domain.dll"#r "..\MetaGeneticSharp\src\MetaGeneticSharp.Extensions\bin\Debug\net9.0\MetaGeneticSharp.Infrastructure.dll"#r "..\MetaGeneticSharp\src\MetaGeneticSharp.Extensions\bin\Debug\net9.0\MetaGeneticSharp.Domain.dll"#r "..\MetaGeneticSharp\src\MetaGeneticSharp.Extensions\bin\Debug\net9.0\MetaGeneticSharp.Extensions.dll"#r "..\MetaGeneticSharp\src\MetaGeneticSharp.Extensions\bin\Debug\net9.0\System.Drawing.Common.dll"#r "..\MetaGeneticSharp\src\MetaGeneticSharp.Extensions\bin\Debug\net9.0\SkiaSharp.dll"using MetaGeneticSharp;// KnownFunctions, ShiftedFitness, RotatedFitness, RotationMatrices, SkiaLandscapeRendererusing GeneticSharp;// IFitness, ChromosomeBase, Gene, RandomizationProviderusing System.Runtime.InteropServices;// NativeLibrary, RuntimeInformation// .NET Interactive quirk: a #r to the managed SkiaSharp.dll does NOT wire up SkiaSharp's// runtimes/<rid>/native/ probing, so the first Skia call would P/Invoke a native lib that was// never loaded (BadImageFormatException). We preload the arch-matching native binary once.string rid = RuntimeInformation.ProcessArchitecture== Architecture.Arm64?"win-arm64": RuntimeInformation.ProcessArchitecture== Architecture.X86?"win-x86":"win-x64";NativeLibrary.Load($@"..\MetaGeneticSharp\src\MetaGeneticSharp.Extensions\bin\Debug\net9.0\runtimes\{rid}\native\libSkiaSharp.dll");// DoubleArrayChromosome: minimal continuous chromosome (bare double genes) with per-gene bounds,// so the fitness decorators can evaluate any 2-D point. Same chromosome as MGS-8/MGS-12.publicclass DoubleArrayChromosome : ChromosomeBase{privatereadonlydouble _min;privatereadonlydouble _max;publicDoubleArrayChromosome(double[] values,double min,double max):base(values.Length){ _min = min; _max = max;for(int i =0; i < values.Length; i++)ReplaceGene(i,newGene(values[i]));}publicoverride IChromosome CreateNew(){var r = RandomizationProvider.Current;var v =newdouble[Length];for(int i =0; i < Length; i++) v[i]= r.GetDouble(_min, _max);returnnewDoubleArrayChromosome(v, _min, _max);}publicoverride Gene GenerateGene(int i)=>newGene(RandomizationProvider.Current.GetDouble(_min, _max));publicdouble[]GetDoubleValues()=>GetGenes().Select(g =>(double)g.Value).ToArray();}// Rendering helpers (same recipe as MGS-8). SkiaLandscapeRenderer is the GDI-free, papermill-safe path.stringHeatmapHtml(byte[] png,string caption,int displayWidth){string b64 = Convert.ToBase64String(png);return $"<figure style='margin:6px 0;display:inline-block'>"+ $"<img src='data:image/png;base64,{b64}' style='width:{displayWidth}px;image-rendering:pixelated;border:1px solid #ccc'/>"+ $"<figcaption style='font:12px sans-serif;color:#555'>{caption}</figcaption></figure>";}voidShowPng(byte[] png,string caption,int displayWidth =440)=>display(HTML(HeatmapHtml(png, caption, displayWidth)));// Two heatmaps side by side (unrotated vs rotated) -- the core comparison of this notebook.voidShowPair(byte[] pngA,string capA,byte[] pngB,string capB,int displayWidth =400)=>display(HTML($"<div style='display:flex;flex-wrap:wrap;gap:8px'>{HeatmapHtml(pngA, capA, displayWidth)}{HeatmapHtml(pngB, capB, displayWidth)}</div>"));// Wrap an IFitness into the Func<double[],double> field the renderer expects over [lo,hi]^2.double lo =-5.12, hi =5.12;Func<double[],double>Field(IFitness f)=> c => f.Evaluate(newDoubleArrayChromosome(c, lo, hi));Console.WriteLine("Wiring OK : MetaGeneticSharp + Extensions (KnownFunctions + ShiftedFitness/RotatedFitness + SkiaLandscapeRenderer).");Console.WriteLine($" De-bias tooling : {typeof(ShiftedFitness).Name}, {typeof(RotatedFitness).Name}, {typeof(RotationMatrices).Name}");Console.WriteLine($" Renderer : {typeof(SkiaLandscapeRenderer).Name} (SkiaSharp, native rid={rid})");
Partie A — Séparabilité, rotation : le principe sur le papier
Soit \(f:\mathbb{R}^2\to\mathbb{R}\). On dit que \(f\) est séparable (additivement) s’il existe \(g,h\) avec \(f(x,y)=g(x)+h(y)\). Conséquence pratique : la minimisation se décompose axe par axe — c’est ce qu’exploitent le coordinate descent et les opérateurs génétiques qui ne recombinent qu’un gène à la fois.
Sphere (\(x^2+y^2\)), Rastrigin (\(x^2+y^2\) plus modulation cosinus par axe) et Rosenbrock sont séparables dans leurs axes canoniques. Une rotation orthogonale\(M\) change de repère :
\[f'(x,y) \;=\; f(M\cdot(x,y))\,,\qquad M M^\top = I.\]
Développons sur Sphere, qui vaut \(\|(x,y)\|^2\) : comme \(M\) préserve la norme, \(\|M(x,y)\|=\|(x,y)\|\), donc \(f'\equiv f\) — Sphere est invariante par rotation. Le décorateur RotatedFitness est alors un no-op visible, ce qui en fait notre témoin de contrôle (Partie B).
Sur Rosenbrock en revanche, la « vallée banane » courbe n’est pas invariante : la rotation la pivote hors des axes et déplace l’optimum (Partie C). Sur Rastrigin, les crêtes de cosinus basculent et ne sont plus alignées avec la grille d’échantillonnage axe-par-axe d’un GA (Partie D) — c’est exactement la signature numérique mesurée en MGS-12.
Le décorateur applique \(f'(x)=f(M x)\) tel quel, en réutilisant la fonction canonique : aucune mathématique n’est réimplémentée.
Partie B — Témoin de contrôle : Sphere est invariante par rotation
On rend Sphere, puis Sphere rotatée par une matrice aléatoire reproductible RotationMatrices.Seeded(2, 42) (produit de rotations de Givens, garantie orthogonale). Si le décorateur est correct, les deux paysages sont identiques pixel pour pixel : une rotation ne modifie pas une fonction invariante par rotation.
IFitness sphere =newSphereFitness();double[,] Q = RotationMatrices.Seeded(2,42);// produit de Givens, M*M^T = IIFitness sphereRot =newRotatedFitness(sphere, Q);Console.WriteLine($"Q orthogonale ? {RotationMatrices.IsOrthogonal(Q)} (produit de rotations de Givens).");var bounds =(lo, hi);byte[] pngSphere = SkiaLandscapeRenderer.RenderHeatmapPng(Field(sphere), bounds, bounds,400,320);byte[] pngSphereRot = SkiaLandscapeRenderer.RenderHeatmapPng(Field(sphereRot), bounds, bounds,400,320);ShowPair(pngSphere,"Sphere (axes canoniques)", pngSphereRot,"Sphere rotatée (Seeded 2,42)");// Sanity numérique : en tout point, f_rot(x) == f(x) à 1e-9 près (invariance par rotation).double[][] probes ={new[]{1.0,1.0},new[]{-2.0,3.0},new[]{0.5,-0.5}};foreach(var p in probes) Console.WriteLine($" f({p[0]:G3},{p[1]:G3}) = {Field(sphere)(p):G6} f_rot = {Field(sphereRot)(p):G6} ecart = {Math.Abs(Field(sphere)(p)-Field(sphereRot)(p)):G2}");
Q orthogonale ? True (produit de rotations de Givens).
Lecture. Les deux cartes de chaleur sont identiques et l’écart numérique est nul (~1e-15) : la rotation est bien un no-op sur Sphere. C’est le témoin qui valide le décorateur — toute différence observée plus loin provient bien de la géométrie de la fonction, pas d’un bug de rotation.
Partie C — La vallée banane pivote : Rosenbrock rotatée
Rosenbrock est l’archétype de la fonction non invariante par rotation : son optimum (valeur 0) est en \((1,1)\) et la « vallée banane » courbe longe l’axe \(x\). On rotatée de \(45°\) avec \(M=[[c,-s],[s,c]]\), \(c=s=\sqrt2/2\). L’optimum se déplace alors en \(M^\top(1,1)=(\sqrt2, 0)\) et la vallée pivote hors des axes — un optimiseur axe-par-axe ne peut plus descendre un seul axe à la fois pour atteindre le fond de la vallée.
IFitness rosen =newRosenbrockFitness();double c = Math.Sqrt(2.0)/2.0;double[,] M45 =new[,]{{ c,-c },{ c, c }};// rotation 45°IFitness rosenRot =newRotatedFitness(rosen, M45);var bounds =(lo, hi);byte[] pngRosen = SkiaLandscapeRenderer.RenderHeatmapPng(Field(rosen), bounds, bounds,400,320);byte[] pngRosenRot = SkiaLandscapeRenderer.RenderHeatmapPng(Field(rosenRot), bounds, bounds,400,320);ShowPair(pngRosen,"Rosenbrock (vallee banane axee)", pngRosenRot,"Rosenbrock rotatée 45° (vallee pivotee)");// Vérification : l'optimum rotaté est en M^T*(1,1) = (sqrt2, 0), valeur 0 ; (1,1) n'est plus optimal.Console.WriteLine($"f_rot(sqrt2, 0) = {Field(rosenRot)(new[]{Math.Sqrt(2.0),0.0}):G6} (optimum attendu ~0)");Console.WriteLine($"f_rot(1, 1) = {Field(rosenRot)(new[]{1.0,1.0}):G6} (<0 : (1,1) n'est plus l'optimum)");
f_rot(sqrt2, 0) = -4,97968E-30 (optimum attendu ~0)
f_rot(1, 1) = -201 (<0 : (1,1) n'est plus l'optimum)
Lecture. La vallée courbe est visiblement inclinée après rotation : le fond de la vallée ne suit plus un axe. C’est précisément ce qui casse la séparabilité — il n’existe plus de \(g(x)+h(y)\), un terme croisé \(xy\) est apparu. Un croisement ou une mutation qui ne touche qu’un gène à la fois (axis-aligned) suit désormais une trajectoire oblique par rapport à la vallée et la descente devient inefficace.
Partie D — Les crêtes cosinus basculent : Rastrigin rotatée (le lien avec MGS-12)
Rastrigin ajoute à Sphere une modulation cosinus par axe : \(f(x,y)=\sum_i [x_i^2 - 10\cos(x_i)] + 20\). Les crêtes sont donc alignées avec les axes canoniques. Après rotation, ces crêtes basculent hors de la grille — et c’est exactement la signature numérique mesurée en MGS-12 : le GA axis-aligned perd ~+2847 sur Rastrigin rotatée (il échantillonne une grille qui ne « voit » plus la structure des crêtes), quand le DE rotation-robuste ne perd qu’~+2.
IFitness rast =newRastriginFitness();double[,] Qr = RotationMatrices.Seeded(2,42);IFitness rastRot =newRotatedFitness(rast, Qr);var bounds =(lo, hi);byte[] pngRast = SkiaLandscapeRenderer.RenderHeatmapPng(Field(rast), bounds, bounds,400,320);byte[] pngRastRot = SkiaLandscapeRenderer.RenderHeatmapPng(Field(rastRot), bounds, bounds,400,320);ShowPair(pngRast,"Rastrigin (cretes axees)", pngRastRot,"Rastrigin rotatée (cretes basculees)");Console.WriteLine("Comparer avec MGS-12 : le GA axis-aligned perd ~+2847 sur Rastrigin rotatée, le DE ~+2.");Console.WriteLine("Le visuel ci-contre explique le *pourquoi* : la grille d'echantillonnage axe-par-axe ne suit plus les cretes.");
Comparer avec MGS-12 : le GA axis-aligned perd ~+2847 sur Rastrigin rotatée, le DE ~+2.
Le visuel ci-contre explique le *pourquoi* : la grille d'echantillonnage axe-par-axe ne suit plus les cretes.
Lecture. À gauche, les crêtes cosinus forment une grille régulière alignée sur les axes — un GA qui échantillonne selon les axes « tombe » naturellement dans la structure. À droite, la grille est oblique : un échantillonnage axe-par-axe coupe les crènes en biais et la structure régulière disparaît de son point de vue. C’est le mécanisme géométrique derrière le \(\Delta\) numérique de MGS-12, rendu visible.
Partie E — Composition CEC : déplacement puis rotation
Les benchmarks CEC combinent les deux biais : on déplace d’abord l’optimum hors du centre (ShiftedFitness), puis on rotatée (RotatedFitness). La composition new RotatedFitness(new ShiftedFitness(inner, offset), M) applique \(f'(x)=f(Mx - o)\) : l’optimum atterrit en \(M^\top o\) et le paysage est à la fois déplacé et incliné. Double dé-biais — central-bias et axis-alignment neutralisés d’un coup.
double[] offset ={2.0,0.0};// déplace l'optimum de Spheredouble[,] Mce = RotationMatrices.Seeded(2,7);IFitness cec =newRotatedFitness(newShiftedFitness(newSphereFitness(), offset), Mce);var bounds =(lo, hi);byte[] pngCec = SkiaLandscapeRenderer.RenderHeatmapPng(Field(cec), bounds, bounds,420,320);ShowPng(pngCec,"ShiftedFitness(Sphere,(2,0)) puis RotatedFitness(Seeded 2,7) -- optimum en M^T*(2,0)",440);// Optimum combiné : f'(x)=Sphere(M x - o) est optimale en x = M^{-1} o = M^T o (M orthogonale).// Produit matrice-vecteur explicite M^T o pour ne pas confondre ligne et colonne.double[] xStar ={ Mce[0,0]*offset[0]+ Mce[1,0]*offset[1],// (M^T o)_0 Mce[0,1]*offset[0]+ Mce[1,1]*offset[1]};// (M^T o)_1Console.WriteLine($"x* = M^T*offset = ({xStar[0]:G4}, {xStar[1]:G4}) (offset=(2,0) rotaté)");Console.WriteLine($"f'(x*) = {Field(cec)(xStar):G6} (proche de 0 = optimum combine)");
ShiftedFitness(Sphere,(2,0)) puis RotatedFitness(Seeded 2,7) -- optimum en M^T*(2,0)
Lecture. Le puits de Sphere n’est plus au centre (central-bias vaincu) ni aligné sur les axes (axis-alignment vaincu). Un optimiseur qui démarre systématiquement de l’origine ou qui optimise axe par axe est doublement pénalisé — c’est le niveau de robustesse exigé par les suites CEC modernes.
Conclusion
La rotation n’est pas un caprice de benchmark : c’est un test de séparabilité. Trois images le résument :
Sphere rotatée = Sphere : une fonction invariante par rotation ignore le changement de repère — le témoin de contrôle.
Rosenbrock rotatée : la vallée banane pivote, l’optimum déménage, un terme croisé apparaît — la séparabilité est détruite.
Rastrigin rotatée : les crêtes cosinus basculent hors de la grille d’échantillonnage axe-par-axe — le mécanisme géométrique derrière le \(\Delta\approx+2847\) du GA mesuré en MGS-12.
Là où MGS-12 quantifie la perte de fitness des métaheuristiques axis-aligned, ce notebook la montre. Les décorateurs ShiftedFitness / RotatedFitness rendent ces transformations compositionnelles et réutilisent les fonctions canoniques sans les réimplémenter — la transparence requise pour qu’un benchmark soit honnête (cf. Sørensen 2015, « components over metaphors »).
Suite logique : combiner ce diagnostic visuel avec un repair axis-alignment (estimer \(M\) depuis les données puiscontre-rotater), ou comparer les paysages de-biaisés sur d’autres fonctions (Ackley, Schwefel) — voir les exercices.
Exercices
Rappel (convention C.1) : les cellules ci-dessous sont des stubs à compléter. Elles s’exécutent sans erreur (return null / TODO), le notebook reste valide de bout en bout.
Exercice 1 — Ackley rotatée
Ackley a une structure de « cratère » avec des ondulations concentriques. Rendez son paysage avant/après rotation par RotationMatrices.Seeded(2, 42) et observez si les ondulations restent concentriques (invariantes) ou se déforment. Complétez le stub.
// Exercice 1 : rendre Ackley puis Ackley rotatée côte à côte (cf. Partie D pour le modèle).// Indice : new AckleyFitness() ; Field(f) ; ShowPair(...). Interprétez ensuite l'effet de la rotation.public(byte[] plain,byte[] rotated)RenderAckleyPair(){// TODO étudiant : construire ackley + ackleyRot, rendre les deux PNG, les retourner.return(null,null);// TODO etudiant}// Décommentez après completion :// var (aPlain, aRot) = RenderAckleyPair();// ShowPair(aPlain, "Ackley", aRot, "Ackley rotatée");
Pour Rosenbrock rotatée 45°, l’optimum théorique est en \((\sqrt2, 0)\). Retrouvez-le numériquement par une simple recherche sur grille (pas d’optimiseur) : balayez \([-5,5]^2\) par pas de 0.1 et retournez le point de fitness maximale. Vérifiez qu’il est proche de \((\sqrt2, 0)\approx(1.414, 0)\).
// Exercice 2 : recherche sur grille de l'optimum de Rosenbrock rotatée 45°.// Indice : DoubleArrayChromosome + Field(rosenRot) ; double for sur x,y par pas 0.1 ; garder le max.publicdouble[]GridArgmax(Func<double[],double> field,double lo,double hi,double step){// TODO etudiant : parcourir la grille, retourner le (x,y) de fitness maximale.returnnull;// TODO etudiant}// Décommentez après completion :// double[] opt = GridArgmax(Field(new RotatedFitness(new RosenbrockFitness(), new[,]{{c,-c},{c,c}})), -5, 5, 0.1);// Console.WriteLine($"optimum numerique = ({opt[0]:G4}, {opt[1]:G4}) (theorique (sqrt2,0) ~ (1.414, 0))");
Exercice 3 — Composition CEC sur une autre fonction
Appliquez la composition RotatedFitness(ShiftedFitness(inner, offset), M) à Schwefel (dont l’optimum n’est pas au centre même non déplacé). Rendez le paysage et commentez : où atterrit l’optimum combiné, et la vallée est-elle reconnaissable ?
// Exercice 3 : rendre Schwefel shift+rotate.// Indice : new SchwefelFitness() ; choisir un offset non nul et une RotationMatrices.Seeded(2, seed).publicbyte[]RenderSchwefelShiftedRotated(){// TODO étudiant : composer ShiftedFitness puis RotatedFitness, rendre le PNG.returnnull;// TODO etudiant}// Décommentez après completion :// ShowPng(RenderSchwefelShiftedRotated(), "Schwefel shift+rotate");