Aujourd'hui, l'ingénieur IA est devenu la nouvelle coqueluche du marché. On ne parle plus que de lui, partout. Véritable vedette des entreprises en quête d'innovation, il doit sa popularité croissante à la domination des LLM et à la mort programmée du prompt engineer.
Pourtant, si l'IA semble aujourd'hui se cantonner aux outils des géants de la Tech — OpenAI, Anthropic ou Google — pour générer du code ou déployer des cohortes d'agents au succès parfois mitigé auprès des métiers, il fut un temps (lointain, plus de cinq ans, une éternité) où le Graal absolu était la data-driven company.

Il y avait une croyance quasi mystique dans la capacité d'une random forest et d'un data scientist démiurge à faire parler la donnée et à faire surgir de la valeur du marasme du data lake. Tel un alchimiste équipé d'un Jupyter notebook — puisqu'après tout, le temps de la mise en production viendrait plus tard —, le praticien transformerait le plomb de la donnée en or et guiderait l'organisation vers un monde meilleur en interprétant les arcanes des obscurs fichiers CSV et JSON. C'était une époque incroyable durant laquelle prononcer le mot « Kaggle » vous différenciait de la plèbe. Certains n'y voyaient qu'un réflexe de déglutition et s'inquiétaient pour votre santé**, tandis que d'autres**, initiés, comprenaient que vous parliez d'un site web de compétitions de data science. Mais ces jours fastes ne durèrent pas. On se rendit vite compte que les projets ne quittaient jamais l'étape de l'expérimentation et du notebook. Ils ne passaient jamais en production et restaient au stade de jouets algorithmiques.
Aujourd'hui, les choses ont changé. Des outils comme Vertex AI, AutoML ou BigQuery ML ont rendu le machine learning et, dans une moindre mesure, la data science accessibles en production à un public averti mais non spécialiste. Le nombre d'epochs d'entraînement des modèles a été remplacé par le nombre de tokens dépensés. Les questions sur la manière d'augmenter le nombre de features (les données d'entraînement fournies au modèle) ou sur la façon de les préparer pour que le modèle puisse les utiliser pour son entraînement ou ses prédictions ont fait place à : « mets tout dans un fichier markdown et ça passera ». Mais, si les LLM et les agents sont des outils puissants et versatiles, sont-ils pour autant la panacée ? Dans une ère où le mot IA est devenu synonyme de LLM, les approches classiques de modèles spécialistes ont-elles encore leur place ?
Dans cet article, j'ai voulu retourner dans ces temps anciens et oubliés d'avant 2022, avec un petit retour à kaggle, dans cette compétition: Predicting Stellar Class.
Cette compétition "amicale" avait pour objectif de classer des étoiles en différentes catégories, GALAXY, STAR et QSO
Une ÉTOILE est un objet unique lumineux qui produit de l'énergie grâce à la fusion nucléaire dans son cœur. La fusion transforme des éléments légers en éléments plus lourds et libère d'énormes quantités d'énergie au cours de ce processus. C'est cette énergie qui la fait briller. Notre Soleil est une étoile ordinaire. Lorsque les astronomes observent des étoiles dans le cadre de relevés stellaires, ils observent généralement des objets individuels situés dans notre propre galaxie ou dans des régions relativement proches de l'espace. Comme les étoiles sont comparativement proches, leur lumière n'a pas parcouru d'immenses distances cosmiques, ce qui explique pourquoi elles présentent souvent des valeurs de décalage vers le rouge (redshift) relativement faibles (ces étoiles étant proches, leur décalage spectral est surtout causé par leur mouvement d'éloignement par rapport à nous et à l'effet doppler résultant) . À l'échelle de l'Univers, une étoile constitue l'élément de base à partir duquel les galaxies sont formées.
Une GALAXIE est une structure beaucoup plus massive. Contrairement à une étoile, ce n'est pas un objet unique, mais un immense système lié par la gravité, contenant des étoiles, du gaz, de la poussière, des vestiges stellaires, des planètes et de grandes quantités de matière noire. Une galaxie typique peut renfermer de quelques millions à plusieurs milliers de milliards d'étoiles. La galaxie dans laquelle nous vivons, la Voie lactée, contient plusieurs centaines de milliards d'étoiles. Une analogie utile consiste à considérer une étoile comme une maison individuelle et une galaxie comme une ville entière regroupant des milliards de maisons, ainsi que les routes, les infrastructures et tout ce qui les relie. Lorsqu'un télescope observe une galaxie lointaine, la lumière qui nous parvient est généralement la lumière combinée d'innombrables étoiles, plutôt que celle d'un seul objet. Comme les galaxies sont souvent situées à des distances bien plus grandes que les étoiles individuelles, elles présentent fréquemment des valeurs de décalage vers le rouge (redshift) plus importantes. Pour les galaxies les plus éloignées, ce redshift est principalement dû à l'expansion de l'Univers, qui étire les longueurs d'onde de la lumière au cours de son trajet jusqu'à nous (quand une étoile s'éloigne de nous, le signal part dans les rayons infrarouges, dans les ultra-violets quand elle se rapproche).
Un QSO (Quasi-Stellar Object, ou objet quasi stellaire) est sans doute la classe d'objets la plus fascinante. Nous savons aujourd'hui qu'un quasar est en réalité un type de galaxie active.
Au centre de la plupart des grandes galaxies se trouve un trou noir supermassif. Dans la majorité des cas, ce trou noir est relativement calme. En revanche, dans un quasar, il engloutit activement d'énormes quantités de matière. En spiralant vers le trou noir, le gaz et la poussière forment un disque d'accrétion extrêmement chaud, qui émet un rayonnement intense sur une large gamme de longueurs d'onde. L'énergie produite peut devenir si considérable que la région centrale surpasse en luminosité toute la galaxie qui l'entoure.
Les premiers astronomes ne pouvaient pas distinguer la galaxie hôte. Ils n'observaient qu'un minuscule point lumineux, ressemblant à une étoile. C'est de là que vient le nom « Quasi-Stellar Object », qui signifie « objet ressemblant à une étoile ». Des observations ultérieures ont montré que ces objets n'étaient pas des étoiles, mais des galaxies actives extrêmement éloignées.
Les quasars occupent une place particulière en astronomie, car ils comptent parmi les objets les plus lumineux de l'Univers. Certains peuvent être observés à des distances de plusieurs milliards d'années-lumière. Comme leur lumière a traversé d'immenses distances dans un Univers en expansion, les quasars présentent souvent des valeurs de décalage vers le rouge (redshift) très élevées, bien supérieures à celles des étoiles ordinaires et de nombreuses galaxies. C'est pourquoi le redshift est une caractéristique particulièrement discriminante dans les jeux de données utilisés pour la classification des objets astronomiques.

C'est donc à partir de données tabulaires que l'on doit entraîner un modèle de classification qui prédira le type d'objet stellaire. Il est d'ailleurs assez remarquable de se dire que l'humanité est passée d'investissements en R&D à plusieurs milliards à aujourd'hui un fichier csv pour faire la classification...
Les features fournies étaient:
Résumé des caractéristiques des étoiles
| Caractéristique | Nom en français | Description | Unité / Valeurs |
|---|---|---|---|
| alpha | Ascension droite (RA) | Coordonnée céleste indiquant la position de l'étoile, analogue à la longitude terrestre. | Degrés (°) ou heures (0–24 h) |
| delta | Déclinaison (Dec) | Coordonnée céleste nord/sud. | Degrés (°), de −90° à +90° |
| u | Magnitude bande u | Luminosité ultraviolet. | Magnitude apparente |
| g | Magnitude bande g | Luminosité bande verte-bleue. | Magnitude apparente |
| r | Magnitude bande r | Luminosité bande rouge. | Magnitude apparente |
| i | Magnitude bande i | Luminosité dans le proche infrarouge. | Magnitude apparente |
| z | Magnitude bande z | Luminosité dans la bande infrarouge. | Magnitude apparente |
| redshift | Décalage vers le rouge | Variation de la longueur d'onde due au mouvement relatif ou à l'expansion de l'Univers. Généralement très faible pour les étoiles de la Voie lactée. | Sans unité (z) |
| spectral_type | Type spectral | Classification selon la température de surface et les caractéristiques spectrales. | O, B, A, F, G, K, M (éventuellement L, T, Y) |
| galaxy_population | Population galactique | Population stellaire indiquant l'âge, la métallicité et l'environnement galactique de l'étoile. | Population I, II ou III |
La donnée ressemble à :
| alpha | delta | u | g | r | i | z | redshift | spectral_type | galaxy_population | label | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 147.734256 | 16.959273 | 25.472123 | 21.895559 | 20.357926 | 19.257113 | 18.621057 | 0.408982 | M | Red_Sequence | GALAXY |
| 1 | 127.988677 | 32.346716 | 20.778509 | 19.087062 | 17.587208 | 17.226067 | 16.786433 | 0.157976 | M | Red_Sequence | GALAXY |
| 2 | 179.792648 | 35.344843 | 21.035203 | 21.079128 | 21.171840 | 20.582629 | 20.557366 | 2.823770 | O/B | Blue_Cloud | QSO |
| 3 | 225.818295 | 48.569421 | 23.305056 | 21.050736 | 19.017754 | 18.365658 | 17.914952 | 0.536099 | M | Red_Sequence | GALAXY |
| 4 | 141.836135 | 19.342852 | 21.703158 | 19.471680 | 18.234449 | 17.899447 | 17.616185 | 0.555761 | M | Red_Sequence | GALAXY |
Ces features sont intéressantes, mais elles ne peuvent être utilisées directement. Les modèles ne peuvant lire du texte directement. Il faut donc appliquer du label encoding, sur les label à prédire (la catégorie d'étoile), spectral_type, galaxy_population et label doivent être convertis.
import pandas as pd
df['spectral_type'] = df['spectral_type'].map({'M':0, 'O/B':1, 'G/K':2, 'A/F':3})
df['galaxy_population'] = df['galaxy_population'].map({'Red_Sequence':0, 'Blue_Cloud':1})
df['label'] = df['label'].map({'GALAXY':0, 'QSO':1, 'STAR':2})
Mais ces features, si elles permettent déjà d'avoir une idée du phénomène observé, sont assez limitées et ne permettent pas systématiquement de bien différencier ces astres. C'est pourquoi il faut procéder à une augmentation des features
df['u_g'] = df['u'] - df['g']
df['g_r'] = df['g'] - df['r']
df['r_i'] = df['r'] - df['i']
df['i_z'] = df['i'] - df['z']
df['u_z'] = df['u'] - df['z']
mag_cols = ['u', 'g', 'r', 'i', 'z']
df['mag_std'] = df[mag_cols].std(axis=1)
df['mag_max_min_diff'] = df[mag_cols].max(axis=1) - df[mag_cols].min(axis=1)
Résumé des indices de couleur et features dérivées
| Paramètre | Nom en français | Description | Interprétation |
|---|---|---|---|
| u_g | Indice de couleur u − g | Différence entre magnitude ultraviolet (u) et bleu-vert (g). | Estime la température et la forme du spectre stellaire. |
| g_r | Indice de couleur g − r | Différence entre les bandes g et r. | Très bon indicateur de température stellaire (chaud vs froid). |
| r_i | Indice de couleur r − i | Différence entre les bandes rouge et proche infrarouge. | Aide à distinguer les types d’étoiles et leur évolution. |
| i_z | Indice de couleur i − z | Différence entre proche et plus lointain infrarouge. | Sensible aux étoiles froides et objets rouges. |
| u_z | Indice de couleur u − z | Différence entre ultraviolet et infrarouge. | Mesure globale de la couleur spectrale de l’étoile. |
| mag_std | Écart-type des magnitudes | Dispersion des magnitudes u, g, r, i, z. | Mesure la variabilité spectrale entre bandes. |
| mag_max_min_diff | Amplitude des magnitudes | Différence entre la magnitude maximale et minimale. | Quantifie l’étendue du spectre lumineux. |
Augmenté des nouvelles features, cela nous donne donc:
| alpha | delta | u | g | r | i | z | redshift | spectral_type | galaxy_population | label | u_g | g_r | r_i | i_z | u_z | mag_std | mag_max_min_diff |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 230.038946 | 12.838985 | 24.930610 | 21.672056 | 21.470317 | 21.382870 | 21.422043 | 0.142472 | 3 | 0 | 2 | 3.258554 | 0.201739 | 0.087446 | -0.039173 | 3.508566 | 1.544128 | 3.547739 |
| 357.964012 | 27.573050 | 23.653019 | 21.898960 | 21.691751 | 21.924827 | 20.801286 | 0.160551 | 3 | 1 | 2 | 1.754059 | 0.207209 | -0.233076 | 1.123541 | 2.851733 | 1.034418 | 2.851733 |
| 326.261464 | 0.174719 | 22.358633 | 20.559970 | 20.181406 | 20.238590 | 19.887146 | 0.047593 | 3 | 1 | 2 | 1.798663 | 0.378565 | -0.057184 | 0.351444 | 2.471487 | 0.987193 | 2.471487 |
| 150.634889 | 36.788132 | 22.602868 | 21.234153 | 21.149836 | 21.160034 | 20.778436 | 0.045054 | 3 | 1 | 2 | 1.368715 | 0.084317 | -0.010198 | 0.381598 | 1.824432 | 0.703525 | 1.824432 |
| 117.774495 | 21.773958 | 25.908386 | 23.043032 | 20.986304 | 19.631611 | 19.084015 | 0.055746 | 0 | 0 | 2 | 2.865354 | 2.056728 | 1.354693 | 0.547596 | 6.824372 | 2.790185 | 6.824372 |
Enfin, afin de rendre l'apprentissage plus fiable et éviter qu'une des features domine sur les autres à cause des valeurs qu'elle contient, j'ai appliqué un standarscaler sur les données:
x′=(x−μ)/σ où μ = moyenne de la variable et σ = écart-type
En mettant les variables sur la "même échelle", on réduit l'effet des valeurs élevées sur le modèle, normalise la descente du gradient qui converge plus rapidement.
Pour les besoins de la compétition, cela a été embarqué dans une fonction de transformation.
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
def transformations_pipeline(df, is_train=True, scaler=None):
"""
Cleans, maps, selects features, and scales the dataset for the autoencoders.
Parameters:
-----------
df : pd.DataFrame
The input raw dataframe.
is_train : bool, default=True
If True, fits and returns a new StandardScaler.
If False, uses the passed 'scaler' to transform the data.
scaler : StandardScaler, optional
A pre-fitted scaler object. Required if is_train=False.
Returns:
--------
X_scaled : np.ndarray
The scaled feature array ready for the autoencoder.
y : np.ndarray or None
The target labels (if 'label' exists in the dataframe), else None.
scaler : StandardScaler
The fitted or utilized scaler object.
"""
# 1. Make a copy to avoid SettingWithCopyWarning
processed_df = df.copy()
# 2. Map Categorical Variables
spectral_mapping = {'M': 0, 'O/B': 1, 'G/K': 2, 'A/F': 3}
galaxy_mapping = {'Red_Sequence': 0, 'Blue_Cloud': 1}
processed_df['spectral_type'] = processed_df['spectral_type'].map(spectral_mapping)
processed_df['galaxy_population'] = processed_df['galaxy_population'].map(galaxy_mapping)
# Fill NaN values just in case any unmapped categories slip through
processed_df['spectral_type'] = processed_df['spectral_type'].fillna(-1)
processed_df['galaxy_population'] = processed_df['galaxy_population'].fillna(-1)
processed_df['u_g'] = processed_df['u'] - processed_df['g']
processed_df['g_r'] = processed_df['g'] - processed_df['r']
processed_df['r_i'] = processed_df['r'] - processed_df['i']
processed_df['i_z'] = processed_df['i'] - processed_df['z']
processed_df['u_z'] = processed_df['u'] - processed_df['z']
mag_cols = ['u', 'g', 'r', 'i', 'z']
processed_df['mag_std'] = processed_df[mag_cols].std(axis=1)
processed_df['mag_max_min_diff'] = processed_df[mag_cols].max(axis=1) - processed_df[mag_cols].min(axis=1)
# 3. Define Features (Dropping 'alpha' as an index/ID-like column)
features = ['alpha', 'delta', 'u', 'g', 'r', 'i', 'z', 'redshift', 'spectral_type','galaxy_population', 'u_g', 'g_r', 'r_i', 'i_z', 'u_z','mag_std', 'mag_max_min_diff']
X = processed_df[features].values
# 4. Extract Labels if they exist (Kaggle hidden test sets won't have 'label')
y = processed_df['label'].values if 'label' in processed_df.columns else None
# 5. Handle Scaling
if is_train:
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
else:
if scaler is None:
raise ValueError("A pre-fitted 'scaler' object must be provided when is_train=False.")
X_scaled = scaler.transform(X)
return X_scaled, y, scaler
L'ensemble des données, celles d'entraînement et celles de test du modèle sont ensuite préparées
pipeline_scaler = StandardScaler()
X_train_scaled = pipeline_scaler.fit_transform(X_train)
X_test_scaled = pipeline_scaler.transform(X_test)
Après une grid search qui permet de tester plusieurs combinaisons de modèles, les meilleures performances ont été atteintes pour l'algorithme classique sur kaggle du gradient boosting:
from lightgbm import LGBMClassifier
clf = LGBMClassifier(
class_weight='balanced',
n_estimators=300, # Increased trees
learning_rate=0.03, # Slower learning rate requires more trees but generalizes better
num_leaves=31, # Controls tree complexity
subsample=0.8, # Row masking (adds randomness)
colsample_bytree=0.8, # Feature masking
random_state=42,
n_jobs=-1
)
clf.fit(X_train_final, y_train)
Le classement de la compétition se faisait via la métrique du balanced accuracy score. En effet, les trois catégories n'étant pas présentes dans les mêmes proportions, cette métrique est appropriée, car elle mesure la capacité du modèle à généraliser y compris sur les classes minoritaires. Sur cette compétition, mon modèle a eu un score de 0.96269 et le meilleur de 0.97075.
Au delà d'un certain voyage dans le temps (et les étoiles) nostalgique à l'époque du data scientist roi et du notebook ultime, et en taquinant en passant les nouvelles coqueluches de linkedin qui sont les AI Engineers.

Cette expérience illustre bien l’évolution des approches en machine learning au cours des dernières années, entre exploration de modèles spécialisés et intégration progressive d’outils plus industrialisés et accessibles.
Les méthodes classiques, combinées à un travail rigoureux de préparation des données et d’ingénierie des variables, restent aujourd’hui encore très performantes sur des problématiques structurées. Elles offrent également des avantages importants en termes de contrôle, de transparence et de déploiement local, notamment dans des contextes contraints en ressources ou nécessitant une forte interprétabilité.
Par ailleurs, l’écosystème actuel, marqué par l’essor des modèles de fondation et des outils automatisés de machine learning, ne remplace pas ces approches mais les complète. Il permet d’accélérer certaines étapes du cycle de développement, tout en ouvrant de nouvelles possibilités en matière d’applications et d’intégration en production.
Au-delà des évolutions technologiques, ce type de projet rappelle que la valeur d’un modèle ne réside pas uniquement dans sa complexité, mais dans sa capacité à répondre efficacement à un problème donné, avec des choix méthodologiques adaptés aux contraintes et aux objectifs du cas d’usage.