MGS-12 : Le test de l’alignement d’axes – un optimiseur résiste-t-il à la rotation du paysage ?

← MGS-11 Synergie d’îles | MGS-1 Introduction →

Famille : banc de benchmark dé-biaisé (suite de MGS-10 — biais central).

MGS-10 a montré qu’un optimum placé à l’origine flatte les optimiseurs dont les opérateurs ou l’initialisation sont centrés en zéro (biais central). Il existe un second biais, complémentaire et tout aussi répandu : le biais d’alignement d’axes. Beaucoup d’opérateurs génétiques (crossover uniforme, mutation composante par composante, encerclement WOA) traitent chaque coordonnée indépendamment — ils sont « alignés sur les axes ». Lorsque le paysage présente une structure dans une direction non alignée avec les axes (la vallée en banane de Rosenbrock, qui courbe le long d’une parabole), ces opérateurs la traversent au lieu de la suivre.

La technique standard pour mesurer ce biais (compétitions CEC) : rotationner le paysage par une matrice orthogonale \(M\) (\(M M^{\top} = I\)). La rotation préserve les distances et la valeur de l’optimum, mais déplace le bassin dans une direction non alignée avec les axes. Ce notebook met en œuvre ce banc via le décorateur RotatedFitness du fork (analogue de rotation de ShiftedFitness), et pose deux questions :

  1. Sanity — une rotation change-t-elle un paysage radial comme Sphere ? Non, par construction : c’est le contrôle que le décorateur est correct. Et les fonctions à terme périodique par axe (Rastrigin, Ackley) ? Si — la rotation brise leur séparabilité, et c’est déjà, en soi, le premier signal du biais.
  2. Discrimination — sur un paysage asymétrique (Rosenbrock), la rotation déplace l’optimum : quels optimiseurs le retrouvent ?
// Câblage : DLLs MetaGeneticSharp + GeneticSharp + Extensions (KnownFunctions + RotatedFitness + RotationMatrices).
// Prérequis de build (depuis la racine du fork) : dotnet build ..\MetaGeneticSharp\MetaGeneticSharp.sln
#r "..\MetaGeneticSharp\src\MetaGeneticSharp.Domain\bin\Debug\net9.0\GeneticSharp.Infrastructure.Framework.dll"
#r "..\MetaGeneticSharp\src\MetaGeneticSharp.Domain\bin\Debug\net9.0\GeneticSharp.Domain.dll"
#r "..\MetaGeneticSharp\src\MetaGeneticSharp.Domain\bin\Debug\net9.0\MetaGeneticSharp.Infrastructure.dll"
#r "..\MetaGeneticSharp\src\MetaGeneticSharp.Domain\bin\Debug\net9.0\MetaGeneticSharp.Domain.dll"
#r "..\MetaGeneticSharp\src\MetaGeneticSharp.Extensions\bin\Debug\net9.0\MetaGeneticSharp.Extensions.dll"

using MetaGeneticSharp;
using GeneticSharp;
using System;
using System.Collections.Generic;
using System.Linq;

Console.WriteLine("DLLs chargees. Decorateurs de-biais : ");
Console.WriteLine("  RotatedFitness             : " + typeof(RotatedFitness).Name);
Console.WriteLine("  RotationMatrices           : " + typeof(RotationMatrices).Name);
Console.WriteLine("  ShiftedFitness (MGS-10)    : " + typeof(ShiftedFitness).Name);
Console.WriteLine("Optimiseurs                  : " + typeof(RandomSearchOptimizer).Name);
Console.WriteLine("Fonctions asymetriques       : " + typeof(RosenbrockFitness).Name + " / " + typeof(SchwefelFitness).Name);
DLLs chargees. Decorateurs de-biais : 
  RotatedFitness             : RotatedFitness
  RotationMatrices           : RotationMatrices
  ShiftedFitness (MGS-10)    : ShiftedFitness
Optimiseurs                  : RandomSearchOptimizer
Fonctions asymetriques       : RosenbrockFitness / SchwefelFitness

Le décorateur de rotation : \(f_{\text{rot}}(x) = f(M x)\)

RotatedFitness enveloppe n’importe quelle fonction de fitness et l’évalue sur les coordonnées rotationnées \(y = M x\). La matrice \(M\) est orthogonale (\(M M^{\top} = I\)) : la transformation préserve les distances (norme), donc la valeur de l’optimum est inchangée, mais sa position est déplacée. La factory RotationMatrices.Seeded(n, graine) construit une telle matrice reproductible comme produit de rotations de Givens (une par paire d’indices) — produit de matrices orthogonales = orthogonal, garanti par construction.

Comme ShiftedFitness, c’est un décorateur compositionnel fin : les mathématiques de la fonction interne sont réutilisées telles quelles (jamais réimplémentées), seules les coordonnées qu’on lui fournit sont transformées. Les deux se composent pour la variante CEC complète « décalée puis rotationnée » : new RotatedFitness(new ShiftedFitness(inner, décalage), M).

// DoubleArrayChromosome : chromosome minimal stockant des gènes double nus.
// (Réplique de l'assistant de test du fork ; bornes par gène pour que CreateNew()
//  randomise la population initiale — voir MGS-6 / MGS-10 pour le détail.)
public class DoubleArrayChromosome : ChromosomeBase
{
    private readonly double _min;
    private readonly double _max;
    public DoubleArrayChromosome(double[] values, double min, double max) : base(values.Length)
    {
        _min = min; _max = max;
        for (int i = 0; i < values.Length; i++) ReplaceGene(i, new Gene(values[i]));
    }
    public override IChromosome CreateNew()
    {
        var rand = RandomizationProvider.Current;
        int n = Length;
        var vals = new double[n];
        for (int i = 0; i < n; i++) vals[i] = rand.GetDouble(_min, _max);
        return new DoubleArrayChromosome(vals, _min, _max);
    }
    public override Gene GenerateGene(int geneIndex)
        => new Gene(RandomizationProvider.Current.GetDouble(_min, _max));
    public double[] GetDoubleValues() => GetGenes().Select(g => (double)g.Value).ToArray();
}

var probe = new DoubleArrayChromosome(new double[]{1.0, -2.0}, -2.048, 2.048);
Console.WriteLine("DoubleArrayChromosome defini. Probe : " + probe.GetDoubleValues().Length + " genes.");
DoubleArrayChromosome defini. Probe : 2 genes.

Réduire chaque optimiseur à un délégué Optimizer

Comme dans MGS-10, on emballe chaque construction de métaheuristique dans le contrat Optimizer du banc (taille de population 50, budget d’évaluations partagé). On garde un sous-ensemble représentatif qui couvre toute la gamme de structure d’opérateur :

  • GA à crossover uniforme — opérateur composante par composante (il échange des gènes), typiquement aligné sur les axes ;
  • BBPSO — échantillonnage gaussien par dimension (une loi normale par coordonnée, covariance diagonale) : son apparence « continue » ne le rend pas isotrope, un tirage par axe reste de la structure alignée sur les axes ;
  • WOA — encerclement par différence de positions, mais le coefficient d’encerclement est appliqué composante par composante (structure mixte) ;
  • DE — mutation par différence vectorielle \(F(a-b)\) : la seule dont le déplacement soit véritablement équivariant par rotation.

La recherche aléatoire sert de contrôle non biaisé. La partie D tranchera, à dimension assez élevée, lesquels résistent vraiment.

const int PopSize = 50;   // taille de population (GA + composés)
const int NFE = 2000;    // budget d'évaluations (intentionnellement modeste : on veut
                         //  voir un écart de convergence, pas résoudre Rosenbrock à 0)

IMetaHeuristic BuildGA(int g)     => new DefaultMetaHeuristic();
IMetaHeuristic BuildWOA(int g)    => MetaHeuristicsService.CreateMetaHeuristicByName("WhaleOptimisation",       maxGenerations: g, populationSize: PopSize);
IMetaHeuristic BuildDE(int g)     => MetaHeuristicsService.CreateMetaHeuristicByName("DifferentialEvolution",   maxGenerations: g, populationSize: PopSize);
IMetaHeuristic BuildBBPSO(int g)  => MetaHeuristicsService.CreateMetaHeuristicByName("BareBonesParticleSwarm",  maxGenerations: g, populationSize: PopSize);

// Emballe n'importe quelle construction d'IMetaHeuristic dans le contrat Optimizer du banc.
Optimizer MakeOptimizer(Func<int, IMetaHeuristic> buildMh)
{
    return (Optimizer)((req) =>
    {
        int gens = Math.Max(1, req.Evaluations / PopSize);
        var (min, max) = req.Bounds;
        double mid = 0.5 * (min + max);
        var adam = new DoubleArrayChromosome(Enumerable.Repeat(mid, req.Dimension).ToArray(), min, max);
        var pop = new MetaPopulation(PopSize, PopSize, adam);
        var ga = new MetaGeneticAlgorithm(
            pop, req.Fitness,
            new EliteSelection(),
            new UniformCrossover(0.5f),
            new UniformMutation(true),
            buildMh(gens));
        ga.Termination = new GenerationNumberTermination(gens);
        ga.Start();
        return ga.BestChromosome.Fitness ?? double.NegativeInfinity;
    });
}

var rs = new RandomSearchOptimizer(seed: 7);
Optimizer RandomOptimizer = (Optimizer)((req) => rs.Run(req));

// Ancrage RNG : reproductibilité d'une exécution à l'autre (valeur pédago : un étudiant
// retrouve les valeurs committées). ResetSeed(42) avant le smoke test.
FastRandomRandomization.ResetSeed(42);
double smokeObj = -MakeOptimizer(BuildGA)(new OptimizerRequest(new SphereFitness(), KnownFunctionsBounds.For(typeof(SphereFitness)), 5, NFE));
Console.WriteLine("Smoke GA/Sphere : objectif = " + smokeObj.ToString("G4") + "  (proche de 0 = OK). Delegues prets : GA, WOA, DE, BBPSO, Random.");
Smoke GA/Sphere : objectif = 0,1384  (proche de 0 = OK). Delegues prets : GA, WOA, DE, BBPSO, Random.

Partie A — Rotation et symétrie : qui résiste, qui se trahit ?

Une fonction purement radiale (dont la valeur ne dépend que de la norme \(\|x\|\)) est invariante par rotation : une matrice orthogonale préserve la norme (\(\|M x\| = \|x\|\)), donc \(f(M x) = f(x)\) point par point. Sphere (\(\sum x_i^2\)) est de ce type — c’est notre contrôle de cohérence : si RotatedFitness changeait Sphere, le décorateur serait buggé.

Mais beaucoup de fonctions « standard » mélangent un terme radial et un terme périodique par axe (les \(\cos(2\pi x_i)\) de Rastrigin et Ackley). Ce terme est séparable — il traite chaque coordonnée indépendamment, donc il est aligné sur les axes. Une rotation mélange les coordonnées et brise cette séparabilité : la fonction n’est alors plus invariante. L’écart qu’on mesure ci-dessous quantifie précisément cela — et c’est déjà, en soi, le biais que la rotation cherche à révéler. On évalue chaque fonction en un point fixe, directement puis à travers RotatedFitness muni d’une matrice rotationnée reproductible.

// Partie A : rotation-invariance sur les fonctions symétriques (évaluation ponctuelle).
// Dimension 2 (la rotation y est un seul angle, le cas le plus lisible).
int dimA = 2;
int rotSeed = 7;
double[,] M = RotationMatrices.Seeded(dimA, rotSeed);
Console.WriteLine("Matrice de rotation M (" + dimA + "D, graine " + rotSeed + ") :");
Console.WriteLine("  [ " + M[0, 0].ToString("F4") + "  " + M[0, 1].ToString("F4") + " ]");
Console.WriteLine("  [ " + M[1, 0].ToString("F4") + "  " + M[1, 1].ToString("F4") + " ]");
Console.WriteLine("  Orthogonale (M·Mᵀ = I) ? " + RotationMatrices.IsOrthogonal(M));

// Point de test (dans les bornes Rosenbrock ; seul le point importe, pas les bornes).
double[] x = { 1.5, -0.7 };
var chrom = new DoubleArrayChromosome(x, -2.048, 2.048);

IFitness sphere = new SphereFitness();
IFitness rastrigin = new RastriginFitness();
IFitness ackley = new AckleyFitness();

// La fitness est NÉGATIVÉE (GeneticSharp maximise) ; on affiche l'objectif = -fitness.
double Obj(IFitness f) => -f.Evaluate(new DoubleArrayChromosome(x, -2.048, 2.048));

Console.WriteLine();
Console.WriteLine("Fonction       objectif(x)   objectif_rot(x)   |écart|");
Console.WriteLine(new string('-', 58));
foreach (var (name, fn) in new (string, IFitness)[] { ("Sphere", sphere), ("Rastrigin", rastrigin), ("Ackley", ackley) })
{
    double direct = Obj(fn);
    double rotated = Obj(new RotatedFitness(fn, M));
    Console.WriteLine(name.PadRight(14) + direct.ToString("F6").PadLeft(12) + rotated.ToString("F6").PadLeft(17)
                      + Math.Abs(direct - rotated).ToString("G2").PadLeft(10));
}
Console.WriteLine();
Console.WriteLine("=> Sphere (radiale) : écart 0, le décorateur est validé. Rastrigin/Ackley : écart non nul,");
Console.WriteLine("   leurs termes cosinus par axe brisent la séparabilité sous rotation (premier signal du biais).");
Matrice de rotation M (2D, graine 7) :
  [ -0,7427  -0,6697 ]
  [ 0,6697  -0,7427 ]
  Orthogonale (M·Mᵀ = I) ? True

Fonction       objectif(x)   objectif_rot(x)   |écart|
----------------------------------------------------------
Sphere            2,740000         2,740000         0
Rastrigin        35,830170        38,740040       2,9
Ackley            6,372836         6,443205      0,07

=> Sphere (radiale) : écart 0, le décorateur est validé. Rastrigin/Ackley : écart non nul,
   leurs termes cosinus par axe brisent la séparabilité sous rotation (premier signal du biais).

Lecture. Trois régimes distincts apparaissent dans le tableau :

  • Sphere : écart nul à la précision machine. C’est la seule fonction purement radiale — le décorateur est validé (une fonction invariante par construction reste invariante).
  • Rastrigin : écart marqué. Son terme \(\sum_i \cos(2\pi x_i)\) est séparable (un cosinus par axe) ; la rotation mélange les axes et brise cette séparabilité. Rastrigin n’est pas invariante par rotation.
  • Ackley : écart faible mais non nul. Dominée par son terme radial \(\exp(-b\|x\|)\) (invariant), elle n’est perturbée que par son petit terme cosinus — d’où un effet réduit.

Voilà déjà une leçon : la rotation n’est pas qu’un artifice de test, elle mesure la séparabilité d’une fonction. Les fonctions séparables (un terme par axe) sont précisément celles qu’un optimiseur aligné sur les axes peut résoudre coordonnée par coordonnée ; les rotationner détruit cet avantage. C’est exactement pourquoi les suites CEC rotationnent systématiquement leurs fonctions.

Partie B — Discrimination : la rotation déplace l’optimum de Rosenbrock

La vallée en banane de Rosenbrock, \(f(x,y) = 100(y - x^2)^2 + (1 - x)^2\), est asymétrique : elle courbe le long d’une parabole, une direction bien précise (non alignée avec les axes après rotation). Son optimum global est en \((1, 1)\) (valeur \(0\)). Appliquons une rotation de 45° : \(M_{45} = \frac{\sqrt 2}{2}\begin{pmatrix}1 & -1\\ 1 & 1\end{pmatrix}\).

  • L’optimum ne disparaît pas (valeur conservée) mais se déplace : \(f_{\text{rot}}(x) = f(M_{45} x)\) est minimale là où \(M_{45} x = (1,1)\), soit \(x = M_{45}^{\top}(1,1) = (\sqrt 2, 0)\).
  • L’ancien optimum \((1,1)\) n’est plus optimal sous rotation : \(f_{\text{rot}}(1,1) = f(M_{45}(1,1)) = f(0, \sqrt 2) \approx 201\).
// Partie B : relocalisation de l'optimum de Rosenbrock sous rotation de 45°.
double s = Math.Sqrt(2.0) / 2.0;
double[,] R45 = { { s, -s }, { s, s } };           // rotation de 45°
Console.WriteLine("R45 orthogonale ? " + RotationMatrices.IsOrthogonal(R45));

IFitness rosen = new RosenbrockFitness();
IFitness rosenRot = new RotatedFitness(rosen, R45);
double ObjAt(IFitness f, double a, double b) => -f.Evaluate(new DoubleArrayChromosome(new[] { a, b }, -2.048, 2.048));

double oldCenter_val = ObjAt(rosen, 1.0, 1.0);          // optimum non rotationné
double oldCenter_rot = ObjAt(rosenRot, 1.0, 1.0);       // (1,1) après rotation
double newCenter_rot = ObjAt(rosenRot, Math.Sqrt(2.0), 0.0);  // (√2,0) = nouveau lieu de l'optimum

Console.WriteLine();
Console.WriteLine("Rosenbrock en (1,1)              [ancien optimum]      : " + oldCenter_val.ToString("F4") + "  (= 0, optimum global)");
Console.WriteLine("Rosenbrock roté en (1,1)         [l'ancien n'est plus] : " + oldCenter_rot.ToString("F4") + "  (≈ 201, l'optimum a fui)");
Console.WriteLine("Rosenbrock roté en (√2, 0)       [nouvel optimum]      : " + newCenter_rot.ToString("F4") + "  (≈ 0, l'optimum a déménagé ici)");
Console.WriteLine();
Console.WriteLine("=> la rotation n'a pas détruit l'optimum (valeur conservée ≈ 0), elle l'a DÉPLACÉ de (1,1) à (√2, 0).");
Console.WriteLine("   Un optimiseur aligné sur les axes qui 'savait' descendre vers (1,1) cherche maintenant au mauvais endroit.");
R45 orthogonale ? True

Rosenbrock en (1,1)              [ancien optimum]      : 0,0000  (= 0, optimum global)
Rosenbrock roté en (1,1)         [l'ancien n'est plus] : 201,0000  (≈ 201, l'optimum a fui)
Rosenbrock roté en (√2, 0)       [nouvel optimum]      : 0,0000  (≈ 0, l'optimum a déménagé ici)

=> la rotation n'a pas détruit l'optimum (valeur conservée ≈ 0), elle l'a DÉPLACÉ de (1,1) à (√2, 0).
   Un optimiseur aligné sur les axes qui 'savait' descendre vers (1,1) cherche maintenant au mauvais endroit.

Lecture. La valeur de l’optimum est conservée (\(\approx 0\)) mais sa position a changé. C’est précisément le biais que la rotation expose : un optimiseur dont la dynamique est tacitement alignée avec les axes (init près d’une diagonale d’axes, déplacements composante par composante) « savait » où aller sur le paysage original ; sur le paysage rotationné, cette connaissance tacite est fausse. Reste à mesurer si nos optimiseurs le resentent.

Partie C — Les optimiseurs resentent-ils la rotation ?

On lance chaque optimiseur sur Rosenbrock non rotationné puis rotationné (même matrice M qu’en partie A, graine 7), en dimension 2, avec le même budget NFE. Pour isoler l’effet de la rotation (et pas celui du hasard), on reseme le RNG à l’identique avant chaque paire : les tirages initiaux sont donc les mêmes, seules les valeurs de fitness diffèrent — la différence de résultat est imputable au paysage.

La signature du biais est le delta : \(\Delta = \text{objectif}_{\text{roté}} - \text{objectif}_{\text{non roté}}\). - \(\Delta \approx 0\) : l’optimiseur est robuste à la rotation (ses déplacements ne privilégient pas les axes). - \(\Delta > 0\) : la rotation le dégrade (signature d’un alignement sur les axes).

// Partie C : signature delta (roté - non roté) par optimiseur sur Rosenbrock 2D.
var bounds = KnownFunctionsBounds.For(typeof(RosenbrockFitness));   // (-2.048, 2.048)
double[,] Mc = RotationMatrices.Seeded(dimA, rotSeed);              // même rotation qu'en partie A
int dimC = 2;
int pairSeed = 42;

var optsC = new (string Name, Optimizer Opt)[]
{
    ("Random(contrôle)", RandomOptimizer),
    ("GA(uniform)",      MakeOptimizer(BuildGA)),
    ("WOA",              MakeOptimizer(BuildWOA)),
    ("DE",               MakeOptimizer(BuildDE)),
    ("BBPSO",            MakeOptimizer(BuildBBPSO)),
};

Console.WriteLine("Optimiseur        obj_nonroté   obj_roté     Δ = roté - non roté");
Console.WriteLine(new string('-', 60));
var deltas = new List<(string Name, double D)>();
foreach (var (name, opt) in optsC)
{
    // Même graine avant chaque moitié de la paire -> tirages identiques, paysage seul diffère.
    FastRandomRandomization.ResetSeed(pairSeed);
    double objUnrot = -opt(new OptimizerRequest(new RosenbrockFitness(), bounds, dimC, NFE));
    FastRandomRandomization.ResetSeed(pairSeed);
    double objRot = -opt(new OptimizerRequest(new RotatedFitness(new RosenbrockFitness(), Mc), bounds, dimC, NFE));
    double d = objRot - objUnrot;
    deltas.Add((name, d));
    Console.WriteLine(name.PadRight(18) + objUnrot.ToString("F4").PadLeft(12) + objRot.ToString("F4").PadLeft(12) + d.ToString("F4").PadLeft(13));
}
Console.WriteLine();
Console.WriteLine("Δ > 0 = la rotation dégrade l'optimiseur (alignement sur les axes).");
Optimiseur        obj_nonroté   obj_roté     Δ = roté - non roté
------------------------------------------------------------
Random(contrôle)        0,0557      0,0154      -0,0403
GA(uniform)             0,0776      0,0341      -0,0435
WOA                     0,0000      0,0010       0,0010
DE                      0,0000      0,0000      -0,0000
BBPSO                   0,0179      0,0013      -0,0165

Δ > 0 = la rotation dégrade l'optimiseur (alignement sur les axes).

Lecture honnête : que dit le delta ?

Les données committées ci-dessus montrent un \(\Delta\) quasi nul pour tous les optimiseurs — aucun n’est dégradé par la rotation. Ce n’est pas un échec du banc, c’est le résultat attendu à ce budget et en dimension 2 : Rosenbrock 2D est assez facile pour que tous ces optimiseurs généraux (GA, WOA, DE, BBPSO) la résolvent essentiellement à zéro (objectifs de \(0{,}00\) à \(\sim 0{,}08\)), rotationnée ou non. Il n’y a alors pas de marge pour qu’un alignement sur les axes pénalise — quand tout le monde trouve l’optimum, la rotation ne peut pas discriminer.

La lecture méthodologique est importante : un banc de benchmark ne « prouve » un biais opérateur que si le problème est assez dur pour que le biais ait de la place pour se manifester. Ici le signal net est dans la géométrie du paysage (parties A et B) : la rotation brise la séparabilité des fonctions (Rastrigin) et déplace l’optimum des fonctions asymétriques (Rosenbrock). Pour voir l’effet opérateur — un \(\Delta\) qui sépare les déplacements composante par composant (crossover uniforme, WOA) des déplacements vectoriels (DE) — il faut complexisser le problème : dimension plus élevée et budget plus serré. C’est précisément l’objet de l’exercice 2.

On retrouve l’esprit de MGS-10 : ce n’est pas l’étiquette de la métaphore qui prédit le biais, c’est la structure de l’opérateur — et un banc honnête doit poser un problème assez dur pour que cette structure soit mise à l’épreuve.

→ La partie D ci-après complexifie le problème (dimension 5) et révèle effectivement cette signature opérateur.

Partie D — La signature révélée : plus haute dimension

La partie C, en dimension 2, n’a montré aucun écart : tous les optimiseurs résolvent Rosenbrock 2D (rotationnée ou non), il n’y a pas de marge pour qu’un alignement sur les axes pénalise. C’est la leçon méthodologique rappelée plus haut : un banc ne prouve un biais opérateur que si le problème est assez dur.

Montons en dimension 5, avec des bornes élargies \([-30, 30]\) (l’optimum rotationné a pour norme \(\sqrt d\), il faut de la place) et le même budget modeste. À cette dimension Rosenbrock n’est plus résolue à zéro — il reste de la marge. On rejoue le protocole (même RNG resemée avant chaque moitié de la paire) sur 5 graines et l’on prend la médiane, pour réduire le bruit. C’est le régime CEC canonique où la rotation discrimine.

// Partie D : signature delta en dimension 5 (bornes elargies, 5 graines, mediane).
int dimD = 5;
var boundsD = (-30.0, 30.0);
double[,] Md = RotationMatrices.Seeded(dimD, 7);
int[] seedsD = { 0, 1, 7, 42, 99 };

var optsD = new (string Name, Optimizer Opt)[]
{
    ("Random(contrôle)", RandomOptimizer),
    ("GA(uniform)",      MakeOptimizer(BuildGA)),
    ("WOA",              MakeOptimizer(BuildWOA)),
    ("DE",               MakeOptimizer(BuildDE)),
    ("BBPSO",            MakeOptimizer(BuildBBPSO)),
};

Console.WriteLine("dim=" + dimD + " bornes=(" + boundsD.Item1 + "," + boundsD.Item2 + ") NFE=" + NFE + " graines=" + string.Join(",", seedsD) + " (médiane)");
Console.WriteLine();
Console.WriteLine("Optimiseur        méd_obj_nonroté   méd_obj_roté    Δ=roté-nonroté");
Console.WriteLine(new string('-', 64));
foreach (var (name, opt) in optsD)
{
    var unrots = new List<double>();
    var rots = new List<double>();
    foreach (int sd in seedsD)
    {
        FastRandomRandomization.ResetSeed(sd);
        unrots.Add(-opt(new OptimizerRequest(new RosenbrockFitness(), boundsD, dimD, NFE)));
        FastRandomRandomization.ResetSeed(sd);
        rots.Add(-opt(new OptimizerRequest(new RotatedFitness(new RosenbrockFitness(), Md), boundsD, dimD, NFE)));
    }
    unrots.Sort(); rots.Sort();
    double medU = unrots[unrots.Count / 2];
    double medR = rots[rots.Count / 2];
    double d = medR - medU;
    Console.WriteLine(name.PadRight(18) + medU.ToString("G4").PadLeft(16) + medR.ToString("G4").PadLeft(15) + d.ToString("G4").PadLeft(16));
}
Console.WriteLine();
Console.WriteLine("Δ >> 0 = la rotation dégrade l'optimiseur (signature d'alignement sur les axes).");
dim=5 bornes=(-30,30) NFE=2000 graines=0,1,7,42,99 (médiane)

Optimiseur        méd_obj_nonroté   méd_obj_roté    Δ=roté-nonroté
----------------------------------------------------------------
Random(contrôle)         1,276E+05      9,331E+04      -3,431E+04
GA(uniform)                  763,4           3610            2847
WOA                          6,218           1295            1289
DE                           1,883          3,933            2,05
BBPSO                        13,39          994,9           981,5

Δ >> 0 = la rotation dégrade l'optimiseur (signature d'alignement sur les axes).

Lecture. Cette fois le signal apparaît nettement. À la dimension 5 il reste de la marge (les objectifs sont loin de zéro), et la rotation ne touche pas tous les opérateurs de la même façon :

  • DE (mutation par différence vectorielle \(F(a-b)\)) est le seul clairement robuste (\(\Delta \approx 2\)) : son déplacement est une combinaison linéaire de positions, équivariante par rotation — la direction du pas suit le paysage, pas les axes.
  • GA à crossover uniforme (\(\Delta \approx 2847\)), WOA (\(\Delta \approx 1289\)) et BBPSO (\(\Delta \approx 981\)) sont tous dégradés par la rotation :
    • le crossover uniforme échange des composantes (aligné sur les axes) ;
    • BBPSO échantillonne une gaussienne par dimension (covariance diagonale) — un tirage par axe, c’est bien de la structure alignée sur les axes ;
    • WOA, malgré son encerclement par différence de positions, applique son coefficient d’encerclement composante par composante, ce qui laisse assez de structure alignée sur les axes pour être pénalisé.
  • La recherche aléatoire a un \(\Delta\) négatif non interprétable : ce n’est pas un optimiseur, sa « performance » n’est que du bruit d’échantillonnage.

La leçon se précise : pour résister à la rotation, il ne suffit pas d’être « stochastique » ou « continu », il faut un opérateur véritablement vectoriel — un déplacement qui soit une combinaison linéaire de points du paysage (la différence différentielle de DE). Tout ce qui traite les coordonnées axe par axe (crossover uniforme, gaussienne par dimension, coefficient scalaire composante par composante) porte un alignement que la rotation révèle dès que le problème est assez dur. C’est la thèse « components over metaphors » de Sorensen (2015), dans sa version la plus fine : c’est la structure géométrique du déplacement, pas le nom de l’algorithme, qui prédit le biais.

Conclusion : décalage et rotation, les deux biais du banc CEC

Ce notebook complète MGS-10 : ensemble, ils instrumentent les deux techniques de dé-biaisement des bancs modernes (CEC) —

Biais Ce qu’il flatte Décorateur fork Notebook
Central optimum à l’origine, init/centre en zéro ShiftedFitness MGS-10
Alignement d’axes opérateurs composante par composant RotatedFitness MGS-12 (celui-ci)

Les deux sont des décorateurs compositionnels : ils réutilisent les mathématiques des fonctions canoniques sans les réimplémenter, et se composent (new RotatedFitness(new ShiftedFitness(inner, o), M)) pour la variante CEC complète. La leçon tient : on juge un optimiseur sur la structure de ses opérateurs (vectoriels vs composante par composant, centrés vs non), pas sur le nom de sa métaphore — et un banc honnête doit tester les deux biais.

La partie D boucle le propos : à dimension assez élevée pour qu’il reste de la marge, la rotation discrimine les optimiseurs selon la structure de leurs opérateurs (composante par composant vs vectoriel) — la signature que les métaphores ne prédisent pas mais que la géométrie de l’opérateur, elle, révèle.

Exercices

Convention : les exercices sont des cellules à compléter. Squelette fourni, aucune erreur levée — le notebook s’exécute de bout en bout même non terminé.

Exercice 1 : la signature opérateur (vectoriel vs composante par composant)

Ajoutez DE/best/1 ou un crossover arithmétique entier (moyenne pondérée des deux parents, \(\alpha A + (1-\alpha)B\)) au banc de la partie C. Le crossover arithmétique est-il plus ou moins robuste à la rotation que le crossover uniforme ? Mesurez le \(\Delta\) et confrontez à votre intuition sur la structure de l’opérateur.

Indice : le crossover arithmétique produit un descendant sur le segment entre les parents (combinaison convexe) — une opération géométrique indépendante du repère, donc a priori équivariante par rotation.

// Exercice 1 : ajouter un optimiseur à crossover arithmétique au banc de la partie C.
// TODO étudiant : construire un Optimizer utilisant WholeArithmeticCrossover (ou équivalent),
// le lancer sur Rosenbrock non roté puis roté (même protocole ResetSeed(pairSeed) par moitié),
// et afficher son Δ à côté des autres.
// result = null  // TODO étudiant
Console.WriteLine("Exercice 1 à compléter : Δ du crossover arithmétique vs uniforme.");
Exercice 1 à compléter : Δ du crossover arithmétique vs uniforme.

Exercice 2 : la dimension amplifie-t-elle encore le biais ?

La partie D a révélé la signature à la dimension 5. Reprenez son protocole à la dimension 10 (bornes \([-30, 30]\), mêmes 5 graines, médiane). Le \(\Delta\) des opérateurs alignés sur les axes (GA uniforme, BBPSO) grandit-il avec la dimension ? C’est l’intuition CEC : plus la dimension est élevée, plus la rotation est discriminante. Attention au budget : à très haute dimension tout le monde échoue et le signal disparaît — gardez un NFE où il reste de la marge.

Étape 1 : remplacer dimD par 10. Étape 2 : relancer la paire roté/non roté sur les 5 graines. Étape 3 : comparer les Δ à ceux de la dimension 5.

// Exercice 2 : signature delta de Rosenbrock roté en dimension 10 (vs dimension 5 de la partie D).
// TODO étudiant : adapter la boucle de la partie D avec dimD = 10.
Console.WriteLine("Exercice 2 à compléter : Δ en dimension 10 vs dimension 5.");
Exercice 2 à compléter : Δ en dimension 10 vs dimension 5.

Exercice 3 : la variante CEC complète (décalée PUIS rotationnée)

Composez les deux décorateurs : new RotatedFitness(new ShiftedFitness(new RosenbrockFitness(), offset), M) avec un offset non nul (via ShiftVectors.Seeded) et la matrice M. Vérifiez que l’optimum est à la fois décalé et rotationné, et lancez un optimiseur : combine-t-on les deux difficultés ? Quelle borne faut-il pour rester dans le domaine ?

Indice : l’optimum est en \(M^{\top}(1,1) + o\) ; choisir bornes et offset pour qu’il reste atteignable.

// Exercice 3 : variante CEC décalée + rotationnée.
// TODO étudiant : construire new RotatedFitness(new ShiftedFitness(rosen, offset), M),
// localiser le nouvel optimum (Mᵀ·(1,1) + offset), vérifier sa valeur ≈ 0, lancer un optimiseur.
Console.WriteLine("Exercice 3 à compléter : banc CEC shift+rotate complet.");
Exercice 3 à compléter : banc CEC shift+rotate complet.

Liens

  • MGS-10 — Le test du biais central — le décorateur frère ShiftedFitness (décalage de l’optimum), premier biais du banc dé-biaisé.
  • MGS-6 — Benchmarks comparatifs — la grille 10×3 fonctions × optimiseurs, argument « components over metaphors ».
  • MGS-5 — Métaheuristiques composées — WOA/EO/FBI/DE/BBPSO/SA décomposés en primitives.
  • Décorateurs fork : RotatedFitness, ShiftedFitness dans MetaGeneticSharp.Extensions (matrice orthogonale par produit de rotations de Givens).
  • Référence : suites de benchmark CEC (rotation + décalage) ; Sorensen 2015, Metaheuristics—the metaphor exposed.
Retour au sommet