Quand MLPClassifier affiche un ConvergenceWarning, augmenter max_iter n’est qu’une piste parmi plusieurs. Le message signale un arrêt de l’optimisation avant son critère de convergence ; il ne mesure pas directement la qualité des prédictions. Dans le cas ci-dessous, le meilleur réglage sur la validation conserve un avertissement, tandis qu’un autre termine sans avertissement avec un score légèrement inférieur.
L’objectif est de diagnostiquer un problème concret. Pour la structure du réseau et les fonctions d’activation, commencez par le guide du perceptron multicouche. Ici, nous allons modifier l’échelle des données, le budget, puis le solveur, en conservant un test indépendant.
Ce que l’avertissement permet de conclure
Avec Adam et SGD, max_iter borne le nombre de passages sur les données, pas le nombre total de mises à jour des poids. L-BFGS utilise une autre procédure et dispose aussi de max_fun. Comparer 400 itérations de deux solveurs ne constitue donc pas une comparaison à temps de calcul égal. La référence MLPClassifier décrit ces paramètres.
Un avertissement invite à regarder ensemble le budget atteint, la courbe de perte si elle est disponible, la préparation des variables et la qualité hors apprentissage. À l’inverse, l’absence de message ne prouve ni une bonne généralisation ni un optimum global.
Un protocole qui garde le test à l’écart
Nous créons 1 500 observations artificielles et douze variables, puis multiplions leurs colonnes par des facteurs allant de 0,01 à 1 000. Cette transformation change les unités sans ajouter d’information. Les identifiants sont répartis en trois groupes disjoints : 900 pour apprendre, 300 pour choisir et 300 pour le test final.
Chaque standardisation est apprise uniquement sur les 900 observations d’apprentissage, à l’intérieur d’un Pipeline. La validation choisit une configuration selon la balanced accuracy, moyenne des rappels des classes. Le test final est évalué une seule fois sur le modèle sélectionné, sans réentraînement dans cet exemple. Le guide scikit-learn sur les fuites de données explique pourquoi une transformation ajustée sur tout le fichier compromettrait ce protocole.
Les cinq comparaisons et leurs résultats
| Configuration | Itérations effectuées | Avertissement | Balanced accuracy validation |
|---|---|---|---|
| Adam, unités brutes, budget 30 | 30 | Oui | 0,587 |
| Adam, standardisation, budget 30 | 30 | Oui | 0,747 |
| Adam, standardisation, budget 400 | 400 | Oui | 0,913 |
| L-BFGS, standardisation, budget 400 | 171 | Non | 0,907 |
| Régression logistique standardisée | 9 | Non | 0,733 |

La standardisation améliore ici Adam à budget identique. Augmenter ensuite le budget améliore encore la validation. L-BFGS s’arrête sans avertissement, mais son score n’est pas le plus élevé. Selon la règle de choix annoncée, Adam standardisé avec un budget de 400 est retenu ; son score sur le test final est 0,930.
Ces résultats ne démontrent pas qu’Adam bat généralement L-BFGS, ni que la standardisation produit toujours ce gain. L’écart de validation entre les deux meilleurs modèles est faible, sur seulement 300 observations. Pour un projet réel, une validation croisée sur les données de développement et plusieurs graines aideraient à examiner sa stabilité, avant d’ouvrir le test réservé.
Code, découpage CSV et tests gratuits
Télécharger le diagnostic MLP — ZIP gratuit. L’archive contient le programme, les identifiants et partitions dans decoupage.csv, les résultats, la figure et quatre tests. Installez requirements.txt dans un environnement dédié, puis lancez python diagnostic_mlp.py et python test_exemple.py. Le code fonctionne localement après installation.
"""Le test final n'intervient jamais dans le choix du réglage."""
import csv, json, warnings
from pathlib import Path
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.exceptions import ConvergenceWarning
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import balanced_accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
OUT = Path(__file__).resolve().parent
def donnees():
X, y = make_classification(n_samples=1500, n_features=12, n_informative=7,
n_redundant=2, class_sep=1, random_state=17)
X = X * np.logspace(-2, 3, X.shape[1]) # Unités volontairement hétérogènes.
trainval, test = train_test_split(np.arange(len(y)), test_size=.2,
stratify=y, random_state=23)
train, val = train_test_split(trainval, test_size=.25,
stratify=y[trainval], random_state=24)
return X, y, train, val, test
def configurations():
def mlp(solver, iterations):
return MLPClassifier(hidden_layer_sizes=(24,), solver=solver,
max_iter=iterations, alpha=.01, random_state=42,
n_iter_no_change=20, early_stopping=False)
return {
"adam_brut_30": make_pipeline(mlp("adam", 30)),
"adam_standardise_30": make_pipeline(StandardScaler(), mlp("adam", 30)),
"adam_standardise_400": make_pipeline(StandardScaler(), mlp("adam", 400)),
"lbfgs_standardise_400": make_pipeline(StandardScaler(), mlp("lbfgs", 400)),
"logistique_reference": make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000)),
}
def selectionner(modeles, X_train, y_train, X_val, y_val):
lignes = []
for nom, modele in modeles.items():
with warnings.catch_warnings(record=True) as captures:
warnings.simplefilter("always", ConvergenceWarning)
modele.fit(X_train, y_train)
estimateur = modele.steps[-1][1]
lignes.append({
"configuration": nom,
"avertissements_convergence": sum(issubclass(w.category, ConvergenceWarning) for w in captures),
"iterations": int(np.asarray(estimateur.n_iter_).max()),
"balanced_accuracy_validation": float(balanced_accuracy_score(y_val, modele.predict(X_val))),
})
# En cas d'égalité, ordre déclaré des configurations. Aucun accès au test ici.
meilleur = max(lignes, key=lambda r: r["balanced_accuracy_validation"])["configuration"]
return meilleur, lignes
def experience():
X, y, train, val, test = donnees()
modeles = configurations()
meilleur, lignes = selectionner(modeles, X[train], y[train], X[val], y[val])
# Pas de réentraînement : on évalue le modèle sélectionné tel quel une seule fois.
final = float(balanced_accuracy_score(y[test], modeles[meilleur].predict(X[test])))
with (OUT / "decoupage.csv").open("w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["id", "partition"])
for nom, ids in [("apprentissage", train), ("validation", val), ("test", test)]:
writer.writerows((int(i), nom) for i in ids)
fig, ax = plt.subplots(figsize=(10, 5), layout="constrained")
ax.barh([r["configuration"].replace("_", " ") for r in lignes],
[r["balanced_accuracy_validation"] for r in lignes], color="#167c80")
ax.set(xlim=(0, 1), xlabel="Balanced accuracy sur la validation", title="MLP : changer une cause à la fois")
for i, r in enumerate(lignes):
ax.text(.02, i, f'{r["balanced_accuracy_validation"]:.3f} · avertissement : {r["avertissements_convergence"]}',
va="center", color="white")
fig.savefig(OUT / "figure.png", dpi=160)
plt.close(fig)
return {"effectifs": [len(train), len(val), len(test)], "validation": lignes,
"selection": meilleur, "balanced_accuracy_test_final": final}
if __name__ == "__main__":
resultats = experience()
(OUT / "resultats.json").write_text(json.dumps(resultats, indent=2), encoding="utf-8")
print(json.dumps(resultats, indent=2))
Comment adapter le diagnostic à votre fichier
- Contrôlez les entrées. Recherchez valeurs absentes, infinis, unités incohérentes et variables disponibles seulement après la décision. Une optimisation plus longue ne les répare pas.
- Gardez un modèle de référence. Une régression logistique permet de voir si le réseau apporte quelque chose au problème étudié.
- Changez une cause à la fois. Comparez d’abord la préparation, puis le budget, puis le solveur. Conservez les mêmes partitions pour lire les différences.
- Enregistrez les avertissements. Les masquer avec un filtre global ferait perdre une information utile. Le script les capture et les compte.
- Décidez avant le test. Si vous recommencez les réglages après avoir vu le score final, ce jeu devient à son tour une validation.
Le guide pratique des réseaux scikit-learn souligne leur sensibilité à l’échelle et propose des pistes de solveurs. Il s’agit de points de départ à vérifier sur vos données.
Et l’early stopping ?
Nous l’avons laissé désactivé pour rendre la comparaison des budgets lisible. Avec early_stopping=True, MLPClassifier réserve une fraction de ses données d’apprentissage et suit un score de validation interne. Cette réserve n’est pas votre test final. Évitez d’appeler « convergence » un simple bon score ponctuel : la règle d’arrêt, la perte d’apprentissage et la généralisation répondent à des questions différentes.
Pour approfondir le protocole, lisez le cas XGBoost sur l’early stopping et le test contaminé. Retrouvez les autres exemples dans le catalogue Python gratuit.

