## Mécanique de l'Optimisation

Un modèle qui prédit bien, ce n'est pas qu'une bonne architecture — c'est surtout un apprentissage bien guidé. Comment une machine apprend-elle ? En commettant des erreurs, en les mesurant, et en se corrigeant — encore et encore. C'est cette mécanique qu'on appelle l'**optimisation**.

### Modèles, Données et Apprentissage

Pour démystifier ce qu'est réellement l'apprentissage en machine learning, nous pouvons utiliser une analogie physique simple : le moulage.

* **Les Données (L'original) :** Imaginez que vos données d'entraînement constituent une figurine de référence. Informatiquement, ce sont des matrices de nombres bruts.
* **Le Modèle (La matière) :** Le modèle est la matière brute avec laquelle nous allons fabriquer un moule autour de cette figurine. Cette matière est définie par des nombres modifiables.
* **L'Apprentissage (Le processus) :** Apprendre consiste à presser, déformer et ajuster cette matière itérativement pour qu'elle épouse le plus parfaitement possible la forme de la figurine originale, afin de pouvoir en générer de nouvelles à l'avenir.

::: {.callout-note appearance="simple" icon="false"}
## 🛠️ Le Moule et l'Objet
Imaginez que votre jeu de données est une **pièce mécanique** complexe (avec sa forme globale et ses micro-rayures). Votre modèle d'IA est un **matériau de moulage**. L'apprentissage (les *époques*) consiste à presser ce matériau sur l'objet pour en capturer la forme.
:::

::: {.card .card-window}
:::: {.card-header}
🛠️ Presse en Action
::::

:::: {.card-body .card-vslider-layout}

```{ojs}
//| echo: false
viewof epoch = aptitek.createVerticalSlider({
  label: "Pression",
  min: 0, max: 100, step: 1,
  value: 0,
  direction: "down",
  height: 200
})
```

::::: {#d3-graph-1-container .text-center}
:::::

::::
:::

```{ojs}
//| echo: false
// ==========================================
// 📊 DONNÉES DE SIMULATION
// ==========================================

// Génération de "L'Objet" (Signal + Bruit)
// Forme globale sinusoïdale + variations pointues (rayures/bruit)
objectData = d3.range(0, 100, 2).map(x => ({
  x: x,
  y: 60 + 25 * Math.sin(x / 12) + (x % 8 === 0 ? 12 : -4) // Macro-forme + Micro-texture
}))

// ==========================================
// 🎨 MOTEUR DE RENDU D3
// ==========================================

graph1 = {
  const width = 800;
  const height = 250;

  // Création du SVG
  const svg = d3.create("svg")
    .attr("viewBox", [0, 0, width, height])
    .attr("class", "w-100 h-auto");

  // Échelles
  const xScale = d3.scaleLinear()
    .domain([0, 100])
    .range([50, width - 50]);

  const yScale = d3.scaleLinear()
    .domain([0, 100])
    .range([height - 30, 30]);

  // Générateur de ligne (Spline adoucie)
  const line = d3.line()
    .curve(d3.curveMonotoneX)
    .x(d => xScale(d.x))
    .y(d => yScale(d.y));

  // 1. Rendu de L'Objet Original (La Donnée)
  svg.append("path")
    .datum(objectData)
    .attr("fill", "none")
    // Utilisation stricte des tokens Bootstrap/Solarized
    .attr("stroke", "var(--sol-base01)")
    .attr("stroke-width", 3)
    .attr("stroke-dasharray", "4 4")
    .attr("d", line);

  // 2. Calcul dynamique du Moule (Le Modèle)
  // Interpolation de la position Y basée sur l'état 'epoch'
  const moldData = objectData.map(d => {
    const startY = 100; // Position de repos initiale du moule (très haut)
    const targetY = d.y;
    // Plus epoch augmente, plus currentY s'approche de targetY
    const currentY = startY + (targetY - startY) * (epoch / 100);
    return { x: d.x, y: currentY };
  });

  // 3. Rendu du Moule Dynamique
  svg.append("path")
    .datum(moldData)
    .attr("fill", "none")
    .attr("stroke", "var(--sol-yellow)") // Couleur sémantique Primary
    .attr("stroke-width", 6)
    .attr("stroke-linecap", "round")
    .attr("stroke-linejoin", "round")
    .attr("d", line);

  // Projection dans le DOM
  const container = document.getElementById("d3-graph-1-container");
  if (container) {
    container.replaceChildren(svg.node());
  }

  return svg.node();
}
```

### Poids et Biais

Pour ajuster notre moule à la bonne forme, nous tournons deux types de boutons : les **poids** (qui augmentent ou diminuent l'importance d'une information) et le **biais** (qui décale l'ensemble du moule vers le haut ou le bas).

Dans cette matière à mouler, deux types de paramètres contrôlent tout [@ficus_weights_biases; @glander_parameters] :

* **Les Poids :** Ce sont des "boutons de volume". Ils amplifient ou atténuent l'importance de chaque signal d'entrée. Plus le poids est grand, plus cette entrée compte dans la décision finale. C'est la malléabilité de notre moule.
* **Le Biais :** Un paramètre souvent sous-estimé. Imaginez-le comme le **revenu de base** du modèle — le niveau de réponse minimal même quand toutes les entrées sont nulles. Il donne la flexibilité de décaler la réponse du modèle vers le haut ou le bas.

::: {.callout-note collapse="true"}
## 🔢 Détail mathématique

Le calcul de base d'un neurone s'écrit [@trofimov_2020_neural] :

$$y = w \cdot x + b$$

où $x$ est l'entrée, $w$ le poids (importance), $b$ le biais (décalage). Le but de l'apprentissage : trouver les valeurs de $w$ et $b$ qui minimisent l'erreur.
:::

::: {.card .card-window}
:::: {.card-header}
🎛️ Poids et Biais
::::

:::: {.card-body .card-vslider-layout}

```{ojs}
//| echo: false
viewof bias = aptitek.createVerticalSlider({
  label: "Biais (b)",
  min: -40, max: 40, step: 1,
  value: 0,
  direction: "down",
  height: 200
})
```

::::: {#d3-graph-2-container .text-center}
:::::

```{ojs}
//| echo: false
viewof weight = aptitek.createVerticalSlider({
  label: "Poids (w)",
  min: 0, max: 1.5, step: 0.1,
  value: 0.2,
  direction: "down",
  height: 200
})
```

::::
:::

```{ojs}
//| echo: false
// ==========================================
// 🎨 MOTEUR DE RENDU D3 (Graphique 2)
// ==========================================

graph2 = {
  const width = 800;
  const height = 250;

  const svg = d3.create("svg")
    .attr("viewBox", [0, 0, width, height])
    .attr("class", "w-100 h-auto");

  // Définition des marqueurs pour les flèches vectorielles
  const defs = svg.append("defs");

  // Flèche Bleue (Biais)
  defs.append("marker")
    .attr("id", "arrow-bias")
    .attr("viewBox", "0 0 10 10")
    .attr("refX", 8)
    .attr("refY", 5)
    .attr("markerWidth", 6)
    .attr("markerHeight", 6)
    .attr("orient", "auto")
    .append("path")
    .attr("d", "M 0 0 L 10 5 L 0 10 z")
    .attr("fill", "var(--sol-blue)");

  // Flèche Magenta (Poids)
  defs.append("marker")
    .attr("id", "arrow-weight")
    .attr("viewBox", "0 0 10 10")
    .attr("refX", 8)
    .attr("refY", 5)
    .attr("markerWidth", 5)
    .attr("markerHeight", 5)
    .attr("orient", "auto")
    .append("path")
    .attr("d", "M 0 0 L 10 5 L 0 10 z")
    .attr("fill", "var(--sol-magenta)");

  const xScale = d3.scaleLinear().domain([0, 100]).range([50, width - 50]);
  const yScale = d3.scaleLinear().domain([0, 120]).range([height - 30, 30]);

  const line = d3.line()
    .curve(d3.curveMonotoneX)
    .x(d => xScale(d.x))
    .y(d => yScale(d.y));

  // 1. L'Objet (Toujours en pointillé discret)
  // On réutilise objectData défini dans le bloc OJS du Graphique 1
  svg.append("path")
    .datum(objectData)
    .attr("fill", "none")
    .attr("stroke", "var(--sol-base01)")
    .attr("stroke-width", 2)
    .attr("stroke-dasharray", "3 3")
    .attr("d", line);

  // 2. Mathématiques : Calcul des positions
  const baseLineY = 100 - bias; // Position neutre + Biais

  const moldData2 = objectData.map(d => {
    // La déformation est proportionnelle à la distance entre l'objet et la ligne de repos 100
    const deltaY = d.y - 100;
    const currentY = baseLineY + (deltaY * weight);
    return { x: d.x, y: currentY, targetY: d.y, baseY: baseLineY };
  });

  // 3. Visualisation du Biais (Ligne de base décalée)
  svg.append("line")
    .attr("x1", xScale(0))
    .attr("y1", yScale(baseLineY))
    .attr("x2", xScale(100))
    .attr("y2", yScale(baseLineY))
    .attr("stroke", "var(--sol-blue)")
    .attr("stroke-width", 2)
    .attr("stroke-dasharray", "6 4")
    .attr("opacity", 0.6);

  // Indicateur global de biais (Flèche à gauche)
  svg.append("line")
    .attr("x1", xScale(2))
    .attr("y1", yScale(100))
    .attr("x2", xScale(2))
    .attr("y2", yScale(baseLineY))
    .attr("stroke", "var(--sol-blue)")
    .attr("stroke-width", 3)
    .attr("marker-end", "url(#arrow-bias)");

  // 4. Visualisation des Poids (Flèches d'étirement locales)
  // On ne dessine les flèches que sur certains points pour ne pas surcharger visuellement
  const samplePoints = moldData2.filter((d, i) => i % 6 === 0 && weight > 0);

  svg.selectAll(".weight-vector")
    .data(samplePoints)
    .enter()
    .append("line")
    .attr("class", "weight-vector")
    .attr("x1", d => xScale(d.x))
    .attr("y1", d => yScale(d.baseY))
    .attr("x2", d => xScale(d.x))
    .attr("y2", d => yScale(d.y))
    .attr("stroke", "var(--sol-magenta)")
    .attr("stroke-width", 2)
    .attr("opacity", 0.7)
    .attr("marker-end", "url(#arrow-weight)");

  // 5. Rendu du Moule Final (Le Modèle w*x + b)
  svg.append("path")
    .datum(moldData2)
    .attr("fill", "none")
    .attr("stroke", "var(--sol-yellow)")
    .attr("stroke-width", 4)
    .attr("stroke-linecap", "round")
    .attr("stroke-linejoin", "round")
    .attr("d", line);

  const container = document.getElementById("d3-graph-2-container");
  if (container) {
    container.replaceChildren(svg.node());
  }

  return svg.node();
}
```

### La Fonction de Perte et la Boussole du Gradient

La **fonction de perte** mesure l'erreur globale (combien notre moule est imparfait) tandis que le **gradient** est une boussole qui nous indique dans quelle direction modifier nos réglages pour réduire cette erreur.

Pour ajuster le moule, il faut d'abord mesurer à quel point il est mauvais.

* **La Fonction de Perte :** C'est le thermomètre de l'erreur. Elle mesure l'écart entre la prédiction du modèle et la réalité. Plus l'écart est grand, plus la valeur est élevée. L'objectif : la faire descendre le plus possible [@grover_loss_functions; @wiki_mse; @mitra_2023_unboxing].
* **Le Gradient :** Une fois l'erreur mesurée, comment savoir dans quel sens corriger ? Le gradient est une **boussole** : il indique la direction et la force avec laquelle ajuster chaque paramètre pour réduire l'erreur.

::: {.callout-note collapse="true"}
## 🔢 Ce qu'est un gradient

Le gradient est un vecteur de dérivées partielles de la fonction de perte par rapport à chaque paramètre. Intuitivement : si vous êtes perdu dans un paysage montagneux et voulez atteindre la vallée, le gradient vous dit quelle pente descendre à chaque pas. Mathématiquement, pour un paramètre $w$ : $\frac{\partial \mathcal{L}}{\partial w}$.
:::

::: {.card .card-window}
:::: {.card-header}
🧭 Perte et Gradient
::::

:::: {.card-body .card-vslider-layout}

::::: {#d3-graph-3-container .text-center}
:::::

```{ojs}
//| echo: false
viewof epochG3 = aptitek.createVerticalSlider({
  label: "Époques",
  min: 0, max: 100, step: 1,
  value: 0,
  direction: "down",
  height: 270
})
```

::::
:::

```{ojs}
//| echo: false
// ==========================================
// 🎨 MOTEUR DE RENDU D3 (Graphique 3)
// ==========================================

graph3 = {
  const width = 800;
  const height = 320; // Un peu plus grand pour accueillir la courbe de perte en bas

  const svg = d3.create("svg")
    .attr("viewBox", [0, 0, width, height])
    .attr("class", "w-100 h-auto");

  // Définition des marqueurs de flèches pour le Gradient (Rouge)
  const defs = svg.append("defs");
  defs.append("marker")
    .attr("id", "arrow-gradient")
    .attr("viewBox", "0 0 10 10")
    .attr("refX", 6)
    .attr("refY", 5)
    .attr("markerWidth", 5)
    .attr("markerHeight", 5)
    .attr("orient", "auto")
    .append("path")
    .attr("d", "M 0 0 L 10 5 L 0 10 z")
    .attr("fill", "var(--sol-red)");

  // Échelles de la zone supérieure (Géométrie du moule)
  const xScale = d3.scaleLinear().domain([0, 100]).range([50, width - 50]);
  const yScale = d3.scaleLinear().domain([0, 100]).range([200, 20]);

  // Échelles de la zone inférieure (Courbe de convergence de la Perte)
  const xLossScale = d3.scaleLinear().domain([0, 100]).range([50, width - 50]);
  const yLossScale = d3.scaleLinear().domain([0, 1500]).range([300, 230]);

  const lineGenerator = d3.line()
    .curve(d3.curveMonotoneX)
    .x(d => xScale(d.x))
    .y(d => yScale(d.y));

  // --- 1. CALCULS DES ETATS ET GÉOMÉTRIE ---
  const startY = 95;
  let totalSquareError = 0;

  const currentSimulationData = objectData.map(d => {
    // Évolution du modèle similaire au graphe 1
    const currentY = startY + (d.y - startY) * (epochG3 / 100);
    const error = d.y - currentY; // Distance/Erreur locale
    totalSquareError += error * error;

    return {
      x: d.x,
      targetY: d.y,
      moldY: currentY,
      error: error
    };
  });

  const meanSquaredError = totalSquareError / currentSimulationData.length;

  // Mise à jour dynamique du badge HTML de perte (via classes sémantiques de ton thème)
  const badge = document.getElementById("loss-badge");
  if (badge) {
    badge.textContent = meanSquaredError.toFixed(1);
    badge.dataset.state = meanSquaredError > 100 ? "danger" : "success";
  }

  // --- 2. RENDU GÉOMÉTRIQUE (Zone Supérieure) ---

  // Tracé des espaces vides / Air Gaps (Volume de Perte)
  const areaLoss = d3.area()
    .curve(d3.curveMonotoneX)
    .x(d => xScale(d.x))
    .y0(d => yScale(d.moldY))
    .y1(d => yScale(d.targetY));

  svg.append("path")
    .datum(currentSimulationData)
    .attr("fill", "var(--sol-red)")
    .attr("opacity", 0.15)
    .attr("d", areaLoss);

  // Profil de l'Objet Réel (Données)
  svg.append("path")
    .datum(objectData)
    .attr("fill", "none")
    .attr("stroke", "var(--sol-base01)")
    .attr("stroke-width", 2)
    .attr("stroke-dasharray", "4 4")
    .attr("d", lineGenerator);

  // Vecteurs de Gradients (Forces directionnelles)
  // Échantillonnage pour éviter l'encombrement visuel
  const vectors = currentSimulationData.filter((d, i) => i % 4 === 0 && Math.abs(d.error) > 1);

  svg.selectAll(".gradient-arrow")
    .data(vectors)
    .enter()
    .append("line")
    .attr("class", "gradient-arrow")
    .attr("x1", d => xScale(d.x))
    .attr("y1", d => yScale(d.moldY))
    .attr("x2", d => xScale(d.x))
    // La flèche pointe vers la cible (direction de la descente de gradient)
    .attr("y2", d => yScale(d.targetY - (d.error * 0.15)))
    .attr("stroke", "var(--sol-red)")
    .attr("stroke-width", 1.5)
    .attr("opacity", 0.6)
    .attr("marker-end", "url(#arrow-gradient)");

  // Tracé du Moule (Modèle en cours de compression)
  svg.append("path")
    .datum(currentSimulationData)
    .attr("fill", "none")
    .attr("stroke", "var(--sol-yellow)")
    .attr("stroke-width", 4)
    .attr("d", lineGenerator);

  // --- 3. RENDU DE LA COURBE DE PERTE HISTORIQUE (Zone Inférieure) ---

  // Génération de l'historique complet de la perte de l'époque 0 jusqu'à l'époque actuelle
  const lossHistory = d3.range(0, epochG3 + 1).map(e => {
    let sqErr = 0;
    objectData.forEach(d => {
      const mY = startY + (d.y - startY) * (e / 100);
      const err = d.y - mY;
      sqErr += err * err;
    });
    return { epoch: e, lossValue: sqErr / objectData.length };
  });

  // Séparateur visuel horizontal
  svg.append("line")
    .attr("x1", 50)
    .attr("y1", 215)
    .attr("x2", width - 50)
    .attr("y2", 215)
    .attr("stroke", "var(--sol-base02)")
    .attr("stroke-width", 1)
    .attr("stroke-dasharray", "2 2");

  // Titre du mini-graphe
  svg.append("text")
    .attr("x", 50)
    .attr("y", 226)
    .attr("fill", "var(--sol-base00)")
    .attr("font-family", "var(--font-code, monospace)")
    .attr("font-size", "10px")
    .text("HISTORIQUE DE LA PERTE GLOBALE : J(w,b)");

  // Axes discrets de la courbe de perte
  svg.append("line")
    .attr("x1", 50)
    .attr("y1", yLossScale(0))
    .attr("x2", width - 50)
    .attr("y2", yLossScale(0))
    .attr("stroke", "var(--sol-base02)")
    .attr("stroke-width", 1);

  // Générateur de la courbe de perte
  const lossLineGenerator = d3.line()
    .x(d => xLossScale(d.epoch))
    .y(d => yLossScale(d.lossValue));

  // Tracé de la courbe d'historique de perte
  svg.append("path")
    .datum(lossHistory)
    .attr("fill", "none")
    .attr("stroke", "var(--sol-red)")
    .attr("stroke-width", 2)
    .attr("d", lossLineGenerator);

  // Point d'état actuel sur la courbe de perte
  if (lossHistory.length > 0) {
    const currentLoss = lossHistory[lossHistory.length - 1];
    svg.append("circle")
      .attr("cx", xLossScale(currentLoss.epoch))
      .attr("cy", yLossScale(currentLoss.lossValue))
      .attr("r", 4)
      .attr("fill", "var(--sol-red)");
  }

  // Projection Finale dans l'UI
  const container = document.getElementById("d3-graph-3-container");
  if (container) {
    container.replaceChildren(svg.node());
  }

  return svg.node();
}

```

### La Descente de Gradient et les Optimiseurs Modernes

Pour trouver le réglage parfait, on lâche une bille dans notre paysage d'erreurs : elle roule vers la vallée la plus basse (la descente de gradient). Le **taux d'apprentissage** contrôle la vitesse de la bille, et les **optimiseurs** (comme Adam) ajustent automatiquement cette vitesse pour éviter qu'elle ne se perde.

La **Descente de Gradient** est le mécanisme central : à chaque étape, on ajuste les paramètres dans le sens qui réduit l'erreur [@wiki_gradient_descent; @ruder_2016_gradient].

Imaginez la fonction de perte comme un **paysage montagneux** : chaque combinaison possible de paramètres correspond à un point dans ce relief. Le but est de trouver le point le plus bas (l'erreur minimale). On lâche une "boule" sur la pente, et elle roule vers le bas guidée par le gradient.

Deux réglages clés :

* **Le pas d'apprentissage (Learning Rate) :** La taille des bonds. Trop petit → apprentissage lent. Trop grand → la boule rebondit dans tous les sens sans jamais se stabiliser [@you_learning_rate; @dharanalakota_2025_pinns].
* **L'inertie (Momentum) :** La boule garde un peu de son élan, ce qui lui permet de franchir les petits plateaux et de ne pas rester coincée dans une fausse vallée [@chawla_momentum; @dauphin_saddle_point].

Les **optimiseurs modernes** automatisent ces réglages [@lecture2_optimization; @bottou_2010_sgd] :

* **Adam :** L'optimiseur par défaut aujourd'hui — il adapte automatiquement le pas d'apprentissage pour chaque paramètre [@patsnap_adam].
* **RMSprop :** Stabilise les oscillations en ajustant le pas selon l'historique récent des gradients.
* **AdaGrad :** Efficace quand certaines variables apparaissent rarement (*données éparses*).

::: {.callout-note collapse="true"}
## 🔢 Mécanisme des optimiseurs

La mise à jour des poids à chaque étape s'écrit ([voir le Glossaire](../../glossaire.qmd#symboles-mathématiques-notations)) :
$$w \leftarrow w - \eta \cdot \nabla_w \mathcal{L}$$

* **$\leftarrow$ (flèche gauche)** : l'opérateur d'affectation (le paramètre à gauche prend la nouvelle valeur calculée à droite à chaque étape).
* **$\eta$ (lettre grecque êta)** : le taux d'apprentissage (*learning rate*).
* **$\nabla_w$ (nabla avec indice $w$)** : le gradient de la perte par rapport aux poids $w$ (vecteur des dérivées partielles).
* **$\mathcal{L}$** : la fonction de perte (*loss*).

**Adam** adapte le pas pour chaque paramètre en maintenant deux moyennes mobiles — le gradient moyen ($m_t$) et le gradient carré moyen ($v_t$) :
$$\hat{w} = w - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}$$

* **$t$ en indice** (ex: $m_t$) : représente le pas d'optimisation ou l'itération temporelle en cours.
* **La notation chapeau $\hat{}$** (ex: $\hat{m}_t, \hat{v}_t$) : désigne les moyennes mobiles corrigées pour éliminer le biais d'initialisation à zéro.
* **$\epsilon$ (lettre grecque epsilon)** : une constante de stabilité numérique (ex: $10^{-8}$) empêchant toute division par zéro.
:::

### Simulation de la Trajectoire d'Optimisation

Visualisez de manière interactive comment le choix de l'optimiseur et ses paramètres modifient la trajectoire de la descente dans un paysage de coût en trois dimensions.

::: {.card .card-window .mb-4}

:::: {.card-header}
🏔️ Descente de Gradient 3D
::::

:::: {.tab-actions}

* [Lancer]{#btn-grad-play .bi-play-fill .btn .btn-outline-primary .btn-sm}
* [Pause]{#btn-grad-pause .bi-pause-fill .btn .btn-outline-secondary .btn-sm}
* [Reset]{#btn-grad-reset .bi-arrow-counterclockwise .btn .btn-outline-danger .btn-sm}

::::

:::: {.panel-tabset .opt-type-tabset}

##### Momentum
L'inertie (Momentum) accumule les gradients passés pour accélérer la descente dans les directions constantes et franchir les plateaux ou minima locaux.

##### RMSprop
RMSprop adapte le taux d'apprentissage de chaque paramètre selon la moyenne mobile de la magnitude des gradients récents, stabilisant les oscillations verticales.

##### AdaGrad
AdaGrad adapte le pas d'apprentissage de chaque paramètre selon l'historique de ses gradients accumulés depuis le début (efficace pour les caractéristiques éparses).

##### Adam
Adam combine Momentum et RMSprop en maintenant à la fois une moyenne mobile des gradients (1er moment) et de leurs carrés (2e moment).

::::

:::: {.card-control-row .sim-controls-row}

```{ojs}
//| echo: false

mutable optType = "momentum"

_optTypeWatcher = {
  const w = aptitek.createTabsetWatcher(
    ".opt-type-tabset",
    {
      "Momentum": "momentum",
      "RMSprop": "rmsprop",
      "AdaGrad": "adagrad",
      "Adam": "adam"
    },
    (val) => { mutable optType = val; }
  );
  invalidation.then(() => w.destroy());
  return aptitek.noop();
}

viewof optParam = {
  if (optType === "momentum") {
    return Inputs.range([0.5, 1.0], {
      value: 0.771,
      step: 0.001,
      label: "⚡ Inertie (Momentum)"
    });
  } else if (optType === "rmsprop") {
    return Inputs.range([0.8, 0.999], {
      value: 0.9,
      step: 0.001,
      label: "⚡ Taux de Décroissance (β)"
    });
  } else if (optType === "adagrad") {
    return Inputs.range([0.01, 0.5], {
      value: 0.15,
      step: 0.01,
      label: "⚡ Taux d'Apprentissage (η)"
    });
  } else { // adam
    return Inputs.range([0.5, 0.999], {
      value: 0.9,
      step: 0.001,
      label: "⚡ Premier Moment (β₁)"
    });
  }
}
```

::::

:::: {.px-3 .pb-3}

::::: {.col-12 .mb-3}
:::::: {.card .border .m-0}
::::::: {.card-header .font-monospace .text-uppercase .text-muted .py-2 .small}
🔍 Surface de Coût 3D
:::::::
::::::: {#gradient-descent-container .card-body .gradient-3d-container .p-0}
:::::::
::::::
:::::

::::: {.grid}

:::::: {.g-col-3}
::::::: {.card .card-metric}
[Coût (Loss)]{.card-metric-title}

[7.9000]{#grad-3d-cost .card-metric-value .text-danger}
:::::::
::::::

:::::: {.g-col-3}
::::::: {.card .card-metric}
[θ₁]{.card-metric-title}

[2.200]{#grad-3d-theta1 .card-metric-value .text-primary}
:::::::
::::::

:::::: {.g-col-3}
::::::: {.card .card-metric}
[θ₂]{.card-metric-title}

[1.500]{#grad-3d-theta2 .card-metric-value .text-success}
:::::::
::::::

:::::: {.g-col-3}
::::::: {.card .card-metric}
[Vitesse]{.card-metric-title}

[0.00]{#grad-3d-speed .card-metric-value .text-warning}
:::::::
::::::

:::::

::::

:::

```{ojs}
//| echo: false
import { createGradientSimulation } from "../../assets/js/simulations/gradient-descent.js"

// Reinitialises the Plotly chart only when the optimizer type changes.
// Does NOT depend on optParam — slider changes are handled by _gradPath below.
_gradSim = {
  const sm = createGradientSimulation({
    containerId: "gradient-descent-container",
    metrics: {
      cost:   "grad-3d-cost",
      theta1: "grad-3d-theta1",
      theta2: "grad-3d-theta2",
      speed:  "grad-3d-speed"
    }
  });
  if (!sm) return null;
  const controller = new aptitek.SimulationController(sm, {
    play:  "#btn-grad-play",
    pause: "#btn-grad-pause",
    reset: "#btn-grad-reset"
  });
  invalidation.then(() => controller.destroy());
  return sm;
}

// Updates the descent path whenever optParam (or optType) changes.
// No chart re-initialisation — just recomputes the trajectory and resets the ball.
_gradPath = {
  if (_gradSim) _gradSim.updatePath(optType, optParam);
  return aptitek.noop();
}
```
