MGS-10 : Le test du biais central — un optimiseur « sans biais en zéro » passe-t-il l’épreuve du déplacement ?

Série MetaGeneticSharp | Précédent : MGS-9 - Relief Everest | ↑ Série MGS

MGS-6 comparait les composés (WOA, EO, FBI) au GA « de base » sur dix fonctions, à nombre de générations égal, et montrait qu’aucune métaheuristique « publiée » ne bat systématiquement la composition. Mais un banc qui compte les générations laisse une porte ouverte : si un algorithme concentre ses échantillons près du centre du domaine — là où, par construction, beaucoup de fonctions test placent leur optimum — il peut imiter la performance sans rien comprendre au paysage. C’est la critique de Kudela (Nature Machine Intelligence, 2022) : la plupart des métaheuristiques récentes sont biaisées vers le centre, et ce biais se révèle dès qu’on déplace l’optimum hors du centre.

Ce notebook rend cette critique concrète et mesurable, en câblant le banc d’essai CenterBiasBenchmark du fork (PR #14 + #15, voir #1203) : chaque optimiseur reçoit le même budget en évaluations (NFE, comme mealpy) et est testé sur la fonction centrée (optimum au centre) puis déplacée (optimum relocalisé par un vecteur aléatoire reproductible). La signature du biais est le delta : $= $ objectif(déplacé) \(-\) objectif(centré). En principe, un optimiseur non biaisé couvre le domaine uniformément et obtient \(\Delta \approx 0\) ; un optimiseur biaisé vers le centre fait nettement mieux au centre qu’ailleurs, donc \(\Delta \gg 0\). En pratique — et c’est ce que ce notebook montre — \(\Delta\) ne se lit pas seul : il faut le confronter à la performance absolue, car un optimiseur trop faible a un \(\Delta\) bruité (variance), pas un \(\Delta\) signé (biais).

Objectifs d’apprentissage

  • Comprendre pourquoi un banc à budget égal en générations peut masquer un biais central.
  • Câbler le protocole centré-vs-déplacé (Kudela 2022) via CenterBiasBenchmark.RunSuite et le délégué Optimizer.
  • Mesurer la signature de biais \(\Delta\) pour le GA, WOA, EO, FBI, et apprendre à la lire conjointement à la performance absolue (car un \(\Delta\) élevé peut signifier un biais… ou simplement un optimiseur trop faible).
// Câblage : DLLs MetaGeneticSharp + GeneticSharp + Extensions (KnownFunctions + CenterBiasBenchmark + ShiftedFitness).
// 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. CenterBiasBenchmark : " + typeof(CenterBiasBenchmark).Name);
Console.WriteLine("  ShiftedFitness             : " + typeof(ShiftedFitness).Name);
Console.WriteLine("  RandomSearchOptimizer      : " + typeof(RandomSearchOptimizer).Name);
Console.WriteLine("  KnownFunctionsBounds       : " + typeof(KnownFunctionsBounds).Name);
Console.WriteLine("  ForensicBasedInvestigation : " + typeof(ForensicBasedInvestigation).Name);
Console.WriteLine("  BareBonesParticleSwarm    : " + typeof(BareBonesParticleSwarm).Name);
Console.WriteLine("  SimulatedAnnealing        : " + typeof(SimulatedAnnealing).Name);
DLLs chargees. CenterBiasBenchmark : CenterBiasBenchmark
  ShiftedFitness             : ShiftedFitness
  RandomSearchOptimizer      : RandomSearchOptimizer
  KnownFunctionsBounds       : KnownFunctionsBounds
  ForensicBasedInvestigation : ForensicBasedInvestigation
  BareBonesParticleSwarm    : BareBonesParticleSwarm
  SimulatedAnnealing        : SimulatedAnnealing

Le protocole centré-vs-déplacé (Kudela 2022)

Le banc CenterBiasBenchmark met chaque optimiseur à l’épreuve sur deux versions d’une même fonction :

  1. Centrée — la fonction telle que publiée. Pour Sphere, Rastrigin, Ackley, l’optimum global est à l’origine, qui est aussi le centre du domaine symétrique \([-b, +b]^n\). Un algorithme qui échantillonne près du centre touche donc l’optimum « par construction ».
  2. Déplacée — la même fonction, mais l’optimum est relocalisé hors du centre. ShiftedFitness soustrait un vecteur offset aux coordonnées avant l’évaluation : évaluer \(f(x - \text{offset})\) place l’optimum en \(x = \text{offset}\). Le vecteur est tiré une fois pour toutes par ShiftVectors.Seeded(n, magnitude, seed) — chaque coordonnée dans \([-\text{magnitude}, +\text{magnitude}]\), reproductible (même graine → même déplacement, d’une machine à l’autre).

Les deux runs partagent le même budget en évaluations (NFE), pas le même nombre de générations : c’est la règle d’équité de mealpy, portée par EvaluationBudget. La signature du biais est :

\[\Delta = f^{\star}_{\text{déplacé}} - f^{\star}_{\text{centré}}\]

  • \(\Delta \approx 0\) : l’optimiseur résout la fonction où que soit l’optimum → non biaisé (la recherche aléatoire uniforme est le contrôle canonique, à condition d’un budget suffisant pour approcher l’optimum).
  • \(\Delta \gg 0\) : l’optimiseur résout le cas centré mais échoue une fois déplacé → il exploite le placement central de l’optimum → biaisé vers le centre.

Mise en garde de lecture. \(\Delta\) n’est un signal de biais que si l’optimiseur résolvait effectivement le cas centré. Un optimiseur trop faible — qui n’atteint l’optimum ni centré ni déplacé — a un \(\Delta\) dominé par la variance d’échantillonnage, sans rien révéler d’un biais central. D’où l’importance de lire \(\Delta\) conjointement à la performance absolue (la valeur centrée).

Note d’honnêteté. Le chromosome « adam » qui amorce la population GA est semé au centre du domaine (convention MGS, cf. MGS-6) ; les autres individus sont tirés uniformément par CreateNew(). Cette amorce donne au GA un léger avantage sur le cas centré (l’adam tombe à l’optimum exact pour ces fonctions dont l’optimum est 0 = centre) ; la part dominante de \(\Delta\) vient néanmoins de la dynamique des opérateurs (croisement-moyenne, élitisme) qui, sur un domaine symétrique, ramène la population vers le centre. Le contrôle « recherche aléatoire » n’a aucun adam.

// 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 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, 3.14}, -5.12, 5.12);
Console.WriteLine("DoubleArrayChromosome defini. Probe : " + probe.GetDoubleValues().Length + " genes (borne dans [-5.12, 5.12]).");
DoubleArrayChromosome defini. Probe : 3 genes (borne dans [-5.12, 5.12]).

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

Le banc est agnostique à l’optimiseur : il ne connaît que le contrat delegate double Optimizer(OptimizerRequest) — « consomme ce budget d’évaluations sur cette boîte, renvoie la meilleure fitness ». Nous emballons donc chaque métaheuristique dans une fonction MakeOptimizer qui reconstruit un MetaGeneticAlgorithm à chaque appel (run centré, puis run déplacé), en dérivant le nombre de générations du budget NFE : generations = floor(NFE / populationSize).

Les composés géométriques (WOA, EO, FBI) sont reconstruits par la factory du fork MetaHeuristicsService.CreateMetaHeuristicByName, qui câble automatiquement un convertisseur d’identité gene↔︎double (le câblage que la couche géométrique exige). Le contrôle non biaisé est RandomSearchOptimizer, qui échantillonne uniformément sans aucune dynamique de population.

const int PopSize = 50;   // taille de population (GA + composés)
const int NFE = 5000;    // budget d'évaluations partagé (règle mealpy)  -> generations = 100

// Factory : reconstruit un composé nommé avec le bon MaxGenerations (factory prouvée MGS-5).
IMetaHeuristic BuildGA(int g)  => new DefaultMetaHeuristic();
IMetaHeuristic BuildWOA(int g) => MetaHeuristicsService.CreateMetaHeuristicByName("WhaleOptimisation",           maxGenerations: g, populationSize: PopSize);
IMetaHeuristic BuildEO(int g)  => MetaHeuristicsService.CreateMetaHeuristicByName("EquilibriumOptimizer",       maxGenerations: g, populationSize: PopSize);
IMetaHeuristic BuildFBI(int g) => MetaHeuristicsService.CreateMetaHeuristicByName("ForensicBasedInvestigation", 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);
IMetaHeuristic BuildSA(int g)   => MetaHeuristicsService.CreateMetaHeuristicByName("SimulatedAnnealing",         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;
    });
}

// Contrôle non biaisé : recherche aléatoire uniforme (aucune population, aucune dynamique).
// Même graine pour les runs centré et déplacé -> mêmes points tirés -> comparaison équitable.
var rs = new RandomSearchOptimizer(seed: 7);
Optimizer RandomOptimizer = (Optimizer)((req) => rs.Run(req));

// Smoke test : chaque optimiseur sur Sphere centrée (l'optimum 0 doit être approché).
// Ancre aussi le RNG du smoke test -> tout le notebook est reproductible d'un run a l'autre.
FastRandomRandomization.ResetSeed(42);
double centeredSphere = MakeOptimizer(BuildGA)(new OptimizerRequest(new SphereFitness(), KnownFunctionsBounds.For(typeof(SphereFitness)), 5, NFE));
Console.WriteLine("Smoke GA/Sphere centree     : objectif = " + (-centeredSphere).ToString("G4") + "  (proche de 0 = OK)");
double rsSphere = RandomOptimizer(new OptimizerRequest(new SphereFitness(), KnownFunctionsBounds.For(typeof(SphereFitness)), 5, NFE));
Console.WriteLine("Smoke Random/Sphere centree : objectif = " + (-rsSphere).ToString("G4") + "  (moins bon, normal pour NFE=" + NFE + ")");
Console.WriteLine("Delegues Optimizer prets : GA, WOA, EO, FBI, DE, BBPSO, SA, Random(control).");
Smoke GA/Sphere centree     : objectif = 0,008247  (proche de 0 = OK)
Smoke Random/Sphere centree : objectif = 0,6408  (moins bon, normal pour NFE=5000)
Delegues Optimizer prets : GA, WOA, EO, FBI, DE, BBPSO, SA, Random(control).

L’expérience : la suite centrée-vs-déplacée

Nous lançons CenterBiasBenchmark.RunSuite pour les huit optimiseurs (Random, GA, WOA, EO, FBI, DE, BBPSO, recuit simulé) sur trois fonctions canoniques — Sphere (unimodale lisse), Rastrigin (fortement multimodale), Ackley (puits central entouré de rides) — en dimension 5, avec un budget NFE = 5000 partagé et un déplacement d’amplitude 2.0 (l’optimum se déplace d’au plus 2 unités par axe, reste largement dans les bornes). Chaque fonction reçoit un déplacement distinct (seed + index) afin que les relocalisations ne s’alignent pas sur la diagonale du domaine.

Reproductibilité. Les sept optimiseurs à population tirent leur stochasticité du RNG global du framework, qui par défaut n’est pas seedé. On l’ancre donc une fois avant la suite (FastRandomRandomization.ResetSeed(masterSeed)) : chaque optimiseur consomme alors un flux déterministe, et les chiffres committés sont reproductibles d’une exécution à l’autre — un étudiant qui ré-exécute le notebook retrouve exactement les valeurs de la table. Le contrôle RandomSearchOptimizer reste seedé séparément (seed: 7).

var budget = new EvaluationBudget(NFE);
double shiftMag = 2.0;
int masterSeed = 42;
int dim = 5;

var problems = new List<(IFitness fitness, int dimension)>
{
    (new SphereFitness(),    dim),
    (new RastriginFitness(), dim),
    (new AckleyFitness(),    dim),
};

var optimizers = new (string Name, Optimizer Opt)[]
{
    ("Random(control)", RandomOptimizer),
    ("GA(uniform)",     MakeOptimizer(BuildGA)),
    ("WOA",             MakeOptimizer(BuildWOA)),
    ("EO",              MakeOptimizer(BuildEO)),
    ("FBI",             MakeOptimizer(BuildFBI)),
    ("DE",              MakeOptimizer(BuildDE)),
    ("BBPSO",          MakeOptimizer(BuildBBPSO)),
    ("SA",             MakeOptimizer(BuildSA)),
};

// Reproductibilite : on reseme le RNG global du framework UNE FOIS avant la suite.
// Par defaut RandomizationProvider.Current est un FastRandomRandomization non seede ->
// les nombres re-roulaient a chaque execution. ResetSeed(masterSeed) ancre le flux une fois ;
// chaque optimiseur continue le meme flux determine -> chiffres reproduisibles d'une
// execution a l'autre (valeur pedago : un etudiant retrouve les valeurs committes).
FastRandomRandomization.ResetSeed(masterSeed);
var rows = new List<(string Opt, CenterBiasResult R)>();
foreach (var (optName, opt) in optimizers)
{
    var results = CenterBiasBenchmark.RunSuite(problems, budget, opt, shiftMag, masterSeed);
    foreach (var r in results)
    {
        rows.Add((optName, r));
        Console.WriteLine(optName.PadRight(16) + " " + r.ToRow());
    }
}
Console.WriteLine();
Console.WriteLine("--- Suite terminee : 8 optimiseurs x 3 fonctions, chacune en centre + deplace ---");
Random(control)  SphereFitness    dim= 5  centered=    0,640814  shifted=    0,845416  delta=    0,204602
Random(control)  RastriginFitness dim= 5  centered=   19,403933  shifted=   27,340197  delta=    7,936265
Random(control)  AckleyFitness    dim= 5  centered=    8,609421  shifted=    8,726563  delta=    0,117142
GA(uniform)      SphereFitness    dim= 5  centered=    0,008247  shifted=    0,013545  delta=    0,005298
GA(uniform)      RastriginFitness dim= 5  centered=    1,223560  shifted=    0,430844  delta=   -0,792716
GA(uniform)      AckleyFitness    dim= 5  centered=    1,872916  shifted=    2,292313  delta=    0,419396
WOA              SphereFitness    dim= 5  centered=    0,000000  shifted=    0,001729  delta=    0,001729
WOA              RastriginFitness dim= 5  centered=   11,075693  shifted=   21,668002  delta=   10,592309
WOA              AckleyFitness    dim= 5  centered=    0,000099  shifted=    4,358277  delta=    4,358177
EO               SphereFitness    dim= 5  centered=    0,000000  shifted=    0,000000  delta=    0,000000
EO               RastriginFitness dim= 5  centered=    0,000000  shifted=    1,989918  delta=    1,989918
EO               AckleyFitness    dim= 5  centered=    0,000000  shifted=    0,000001  delta=    0,000001
FBI              SphereFitness    dim= 5  centered=    0,000000  shifted=    0,000000  delta=    0,000000
FBI              RastriginFitness dim= 5  centered=    0,000000  shifted=    0,001021  delta=    0,001021
FBI              AckleyFitness    dim= 5  centered=    0,000000  shifted=    0,002009  delta=    0,002009
DE               SphereFitness    dim= 5  centered=    0,000000  shifted=    0,000000  delta=    0,000000
DE               RastriginFitness dim= 5  centered=    6,441881  shifted=    0,000000  delta=   -6,441881
DE               AckleyFitness    dim= 5  centered=    0,000001  shifted=    0,000003  delta=    0,000001
BBPSO            SphereFitness    dim= 5  centered=    0,000000  shifted=    0,000000  delta=    0,000000
BBPSO            RastriginFitness dim= 5  centered=    7,959662  shifted=    2,984877  delta=   -4,974785
BBPSO            AckleyFitness    dim= 5  centered=    0,000000  shifted=    0,000000  delta=    0,000000
SA               SphereFitness    dim= 5  centered=    0,000194  shifted=    0,000132  delta=   -0,000062
SA               RastriginFitness dim= 5  centered=    3,987544  shifted=    8,964757  delta=    4,977213
SA               AckleyFitness    dim= 5  centered=    0,004804  shifted=    0,005537  delta=    0,000734

--- Suite terminee : 8 optimiseurs x 3 fonctions, chacune en centre + deplace ---
// Tableau de synthese : le delta (signature du biais) par fonction x optimiseur + moyenne par optimiseur.
// (Chart de biais en fin de cellule : SVG inline zero-dep via display(HTML(...)), technique C548-L2.
//  Rend sur GitHub/nbviewer/offline - remplace le CDN-script Plotly (<script src=cdn.plot.ly>
//  sandboxe par GitHub -> rendu BLANC en consultation statique). Cf #6927 / #3801 Prong-A.)
using Microsoft.DotNet.Interactive.Formatting;
using System.IO;
using System.Globalization;
using System.Text;
// Format invariant (decimal DOT) : critique pour le rendu SVG (comma = separateur de coordonnee
// -> casse le parse Chromium sur machine fr-FR). Idiome canon SvgChartHelper.cs (#6942).
string F(double v) => v.ToString("0.###", CultureInfo.InvariantCulture);

var functions = rows.Select(r => r.R.Function).Distinct().ToList();

Console.WriteLine("Delta (objectif deplace - objectif centre) : ~0 = non biaise, >0 = biaise vers le centre.");
Console.WriteLine();
string hdr = "Optimiseur       " + string.Join("", functions.Select(f => f.PadLeft(16))) + "   moyenne";
Console.WriteLine(hdr);
Console.WriteLine(new string('-', hdr.Length));
var byOpt = rows.GroupBy(r => r.Opt).ToList();
double maxMean = byOpt.Max(g => g.Average(x => x.R.Delta));
foreach (var g in byOpt)
{
    var byFn = g.ToDictionary(x => x.R.Function, x => x.R.Delta);
    double mean = g.Average(x => x.R.Delta);
    string row = g.Key.PadRight(16);
    foreach (var fn in functions)
        row += (byFn.TryGetValue(fn, out var d) ? d.ToString("F3") : "-").PadLeft(16);
    row += mean.ToString("F3").PadLeft(10);
    Console.WriteLine(row);
}

// Diagramme en barres horizontales Plotly : signature de biais moyenne par optimiseur.
// Contrairement a la barre ASCII (qui masquait les deltas negatifs via Math.Max(0, mean)),
// le chart SIGNE montre les deux sens du biais : >0 = tire vers le centre (WOA, Random),
// <0 = anti-centre (DE, BBPSO). La ligne zero (zeroline) separe les deux regimes.
{
    // Barres horizontales SIGNEES en SVG inline : >0 = tire vers le centre (bleu),
    // <0 = anti-centre (orange). La zeroline (x=0) separe les deux regimes - contrairement
    // a la barre ASCII (Math.Max(0, mean)) qui masquait les deltas negatifs.
    var ordered = byOpt.OrderByDescending(g => g.Average(x => x.R.Delta)).ToList();
    var names = ordered.Select(g => g.Key).ToArray();
    var means = ordered.Select(g => Math.Round(g.Average(x => x.R.Delta), 3)).ToArray();
    int n = ordered.Count;
    int W = 720, marginL = 110, marginR = 70, marginT = 54, marginB = 48;
    int plotW = W - marginL - marginR;
    int rowH = 34;
    int H = marginT + marginB + n * rowH;
    int plotH = H - marginT - marginB;
    double absMax = means.Max(mv => Math.Abs(mv)); if (absMax < 1e-9) absMax = 1;
    double zeroX = marginL + plotW / 2.0;
    double BarW(double v) => (Math.Abs(v) / absMax) * (plotW / 2.0);
    double BarX(double v) => v >= 0 ? zeroX : zeroX - BarW(v);
    double RowY(int i) => marginT + i * rowH + rowH / 2.0;

    var sb = new StringBuilder();
    sb.Append($"<svg viewBox='0 0 {W} {H}' xmlns='http://www.w3.org/2000/svg' " +
              $"font-family='-apple-system,Segoe UI,sans-serif' font-size='12'>");
    sb.Append($"<rect width='{W}' height='{H}' fill='white'/>");
    sb.Append($"<text x='{W/2}' y='20' text-anchor='middle' font-size='14' font-weight='bold' fill='#333'>" +
              $"Signature de biais moyenne par optimiseur</text>");
    sb.Append($"<text x='{W/2}' y='38' text-anchor='middle' font-size='11' fill='#666'>" +
              $"delta &gt; 0 = centre, &lt; 0 = anti-centre</text>");
    // Grille verticale (5 lignes) + labels axe x
    for (int g = 0; g <= 4; g++)
    {
        double v = -absMax + (2 * absMax) * g / 4.0;
        double gx = marginL + plotW * g / 4.0;
        bool isZero = Math.Abs(v) < absMax * 1e-6;
        sb.Append($"<line x1='{F(gx)}' y1='{marginT}' x2='{F(gx)}' y2='{marginT + plotH}' " +
                  $"stroke='{(isZero ? "#444" : "#e8e8e8")}' stroke-width='{(isZero ? 2 : 1)}'/>");
        sb.Append($"<text x='{F(gx)}' y='{H - marginB + 16}' text-anchor='middle' fill='#666'>{F(Math.Round(v,2))}</text>");
    }
    sb.Append($"<text x='{W/2}' y='{H - 8}' text-anchor='middle' fill='#555'>Delta moyen (objectif deplace - objectif centre)</text>");
    // Barres + labels
    for (int i = 0; i < n; i++)
    {
        double ymid = RowY(i);
        double bw = BarW(means[i]);
        double bx = BarX(means[i]);
        string color = means[i] >= 0 ? "#2a6dba" : "#e8743b";
        sb.Append($"<rect x='{F(bx)}' y='{F(ymid - 11)}' width='{F(bw)}' height='22' fill='{color}' rx='2'/>");
        bool rightSide = means[i] >= 0;
        double lx = rightSide ? bx + bw + 5 : bx - 5;
        string anchor = rightSide ? "start" : "end";
        sb.Append($"<text x='{F(lx)}' y='{F(ymid + 4)}' text-anchor='{anchor}' fill='#333' font-size='11'>{F(means[i])}</text>");
        sb.Append($"<text x='{marginL - 8}' y='{F(ymid + 4)}' text-anchor='end' fill='#333'>{names[i]}</text>");
    }
    display(HTML($"<div>{sb}</div>"));
}
Delta (objectif deplace - objectif centre) : ~0 = non biaise, >0 = biaise vers le centre.

Optimiseur          SphereFitnessRastriginFitness   AckleyFitness   moyenne
---------------------------------------------------------------------------
Random(control)            0,205           7,936           0,117     2,753
GA(uniform)                0,005          -0,793           0,419    -0,123
WOA                        0,002          10,592           4,358     4,984
EO                         0,000           1,990           0,000     0,663
FBI                        0,000           0,001           0,002     0,001
DE                         0,000          -6,442           0,000    -2,147
BBPSO                      0,000          -4,975           0,000    -1,658
SA                        -0,000           4,977           0,001     1,659
Signature de biais moyenne par optimiseurdelta > 0 = centre, < 0 = anti-centre-4.98-2.4902.494.98Delta moyen (objectif deplace - objectif centre)4.984WOA2.753Random(control)1.659SA0.663EO0.001FBI-0.123GA(uniform)-1.658BBPSO-2.147DE

Lecture : que dit réellement le delta ?

Le tableau révèle une histoire plus nuancée que le scénario « propre » (Random ≈ 0, tous les autres ≫ 0) — et c’est précisément cette nuance qui fait la valeur du protocole. Trois régimes apparaissent, selon que l’optimiseur résout ou non la fonction à ce budget (NFE = 5000, dimension 5) :

  1. L’optimiseur est robuste au déplacement → \(\Delta \approx 0\) : il atteint une qualité similaire centré ou déplacé. C’est le cas du GA (uniform) (Sphere \(+0{,}005\), Rastrigin \(-0{,}8\), Ackley \(+0{,}4\)) et surtout de DE (Sphere \(\approx 0\), Rastrigin \(-6{,}4\), Ackley \(\approx 0\)). Ces deux assemblages non métaphoriques n’ont aucune préférence centrale — DE trouve même un meilleur optimum déplacé sur Rastrigin (\(\Delta < 0\)). EO et FBI résolvent eux aussi Sphere et Ackley (\(\Delta \approx 0\) sur ces deux).

  2. L’optimiseur résout le cas centré mais échoue déplacé → \(\Delta \gg 0\) : c’est la signature nette du biais central. Le cas le plus franc est WOA sur Ackley : centré, WOA atteint \(0{,}0\) (l’optimum central) ; déplacé, il plonge à \(4{,}4\). Son bubble-net converge vers la région centrale et rate l’optimum relocalisé. EO montre le même phénomène sur Rastrigin : il y trouve l’optimum centré (\(0{,}0\)) mais échoue déplacé (\(2{,}0\)). FBI, lui, résout Rastrigin centré (\(0{,}0\)) ET déplacé (\(\approx 0{,}0\)) : il est robuste, pas biaisé.

  3. L’optimiseur est trop faible pour résoudre la fonction → \(\Delta\) est du bruit, pas un biais. C’est le Random (control) sur Rastrigin (\(\Delta = 7{,}9\)) : la recherche aléatoire n’atteint jamais l’optimum (meilleur \(\approx 19{-}27\)), donc son \(\Delta\) reflète la variance d’échantillonnage, pas une préférence pour le centre. Sur Sphere et Ackley en revanche, son \(\Delta\) est petit (\(0{,}20\) et \(0{,}12\)) : l’échantillonnage uniforme n’a pas de préférence centrale, donc il obtient un résultat similaire (médiocre, mais semblable) centré ou déplacé.

Leçon. Le biais central n’est ni universel ni absolu : à ce budget, le GA et DE y échappent (ils résolvent ou approchent le problème sans préférence centrale), tandis que les composés métaphoriques (WOA, EO) en sont victimes sur au moins une fonction — FBI, métaphorique lui aussi, y échappe (\(\Delta \le 0{,}002\) partout sur ce banc). Or DE est lui aussi un composé géométrique — même couche que WOA/EO/FBI (GeometricCrossover, MatchMetaHeuristic) — mais son opérateur est une arithmétique nue (\(v = r_1 + F \cdot (r_2 - r_3)\)), sans métaphore. Sa robustesse (\(\Delta \le 0\) partout) écarte donc une cause du biais : ce n’est pas la composition géométrique qui biaise vers le centre. Mais FBI — métaphorique et robuste — écarte la réciproque : la métaphore ne suffit pas à biaiser, c’est l’opérateur qui porte le biais. \(\Delta\) ne se lit pas seul : il faut le confronter à la performance absolue (la valeur centrée). Un \(\Delta\) élevé signe un biais seulement si l’optimiseur résolvait le cas centré ; sinon, il signe juste de la faiblesse. C’est cette lecture croisée — pas le \(\Delta\) brut — qui rend la critique de Kudela (2022) falsifiable.

Confirmer le biais. La signature \(\Delta\) des composés métaphoriques (WOA/Ackley \(4{,}4\), EO/Rastrigin \(2{,}0\)) est partiellement amplifiée par l’amorce « adam au centre » (l’individu initial est à l’optimum exact dans le cas centré) et varie d’une graine à l’autre : voir l’exercice 1 (multi-graine) et l’exercice 2 (amplitude du déplacement) pour vérifier que le biais est structurel, pas un artefact du tirage ou de l’amorce.

Conclusion : le biais central, preuve additionnelle pour « components over metaphors »

MGS-6 montrait qu’aucune métaheuristique « publiée » ne bat systématiquement la composition. MGS-10 ajoute une seconde preuve, plus profonde : le banc centré-vs-déplacé (Kudela 2022) révèle que les composés métaphoriques WOA et EO — reconstruits depuis leurs équations image (bubble-net, équilibre) — sont biaisés vers le centre (\(\bar\Delta\) positif, jusqu’à \(\Delta = 4{,}4\) sur WOA/Ackley), tandis que les assemblages non métaphoriques (GA, DE, BBPSO) sont robustes au déplacement (\(\bar\Delta \le 0\)). FBI, métaphorique lui aussi, échappe au biais sur ce banc (\(\bar\Delta \approx 0\)).

L’apport de DE est décisif. C’est un composé géométrique à part entière — même couche que WOA/EO/FBI (GeometricCrossover, MatchMetaHeuristic, FitnessBasedElitistReinsertion) — mais son opérateur est une arithmétique nue (mutant \(v = r_1 + F \cdot (r_2 - r_3)\)), dénuée de métaphore. Sa robustesse (\(\bar\Delta = -2{,}1\)) écarte une cause du biais : ce n’est pas la composition géométrique qui biaise vers le centre. Mais FBI — composé métaphorique et robuste — écarte la réciproque : la métaphore ne suffit pas à biaiser ; c’est l’opérateur (le bubble-net de WOA, l’équilibre d’EO) qui porte le biais.

La discipline « components over metaphors » sort doublement renforcée : non seulement la métaphore n’apporte rien en performance brute (MGS-6), mais elle cache un biais structurel que seul le protocole centré-vs-déplacé met au jour (MGS-10). Et la règle d’honnêteté tient jusqu’au bout : plutôt que d’invoquer un biais comme argument rhétorique, on le mesure — \(\Delta\) confronté à la performance absolue — pour qu’il cesse d’être invisible.

Exercices

Convention : les exercices sont des cellules à compléter. Squelette fourni, à vous d’implémenter le corps. Les exemples guidés ci-dessus restent des exemples : ne pas les stubber.

Exercice 1 : stabilité de la signature en multi-graine

Le banc ci-dessus tire un seul vecteur de déplacement. La signature \(\Delta\) est-elle stable d’une graine à l’autre, ou un déplacement « chanceux » l’a-t-il gonflée ? Relancez la suite pour plusieurs graines (par ex. 42, 7, 123, 2024) et reportez \(\bar\Delta \pm \sigma\) par optimiseur. En particulier : le \(\Delta = 4{,}3\) de WOA sur Ackley se maintient-il d’une graine à l’autre (biais structurel) ou s’effondre-t-il (artefact) ?

// Exercice 1 : multi-seed (moyenne + écart-type de delta sur N graines).
// TODO étudiant : pour chaque graine dans {42, 7, 123, 2024}, lancer RunSuite sur les memes
//   problemes / optimiseurs, collecter les delta, puis calculer moyenne + ecart-type par optimiseur.
Console.WriteLine("Exercice a completer : stabilite multi-graine de la signature de biais.");
Exercice a completer : stabilite multi-graine de la signature de biais.

Exercice 2 : la signature croît-elle avec l’amplitude du déplacement ?

Si le biais est bien « central », un déplacement plus grand devrait produire un \(\Delta\) plus grand (l’optimum sort davantage de la zone que l’optimiseur couvre bien). Balayez shiftMagnitude dans {0.5, 1.0, 2.0, 4.0} et tracez \(\Delta\) moyen par optimiseur en fonction de l’amplitude. Attention : au-delà d’une certaine amplitude, l’optimum approche la frontière du domaine — interprétez alors avec prudence.

// Exercice 2 : balayage de l'amplitude du déplacement.
// TODO étudiant : pour shiftMag dans {0.5, 1.0, 2.0, 4.0}, relancer RunSuite et tracer le delta moyen.
Console.WriteLine("Exercice a completer : croissance de delta avec l'amplitude du deplacement.");
Exercice a completer : croissance de delta avec l'amplitude du deplacement.

Exercice 3 : étendre le banc à Rosenbrock et Griewank

Ajoutez RosenbrockFitness (vallée plate, optimum en \((1,\dots,1)\) — déjà hors-centre) et GriewankFitness au banc. Comparez : un optimiseur biaisé vers le centre souffre-t-il autant sur Rosenbrock, dont l’optimum n’est pas au centre même dans le cas « centré » ? C’est un contre-exemple instructif : la pathologie mesurée dépend du placement de l’optimum.

// Exercice 3 : étendre la suite de problèmes à Rosenbrock + Griewank.
// TODO étudiant : ajouter (new RosenbrockFitness(), dim) et (new GriewankFitness(), dim) à `problems`,
//   relancer RunSuite, comparer les delta (notamment Rosenbrock, dont l'optimum est hors-centre).
Console.WriteLine("Exercice a completer : etendre le banc centre-vs-deplace.");
Exercice a completer : etendre le banc centre-vs-deplace.

Liens


Référence : Kudela, J. A critical problem with benchmarking optimizers. Nature Machine Intelligence 4, 439–440 (2022) — le protocole centré-vs-déplacé câblé ici via CenterBiasBenchmark (fork MetaGeneticSharp PR #14 + #15, voir #1203).

Retour au sommet