Aller au contenu principal

Module 4 — Entraînement personnalisé et conteneurs

Le carnet du module 2 a validé le prototype ; les données du module 3 sont sur Cloud Storage. Reste à faire tourner l'entraînement ailleurs que sur le carnet : sur une machine dimensionnée pour la tâche, avec un environnement figé, sans dépendre d'un onglet ouvert. C'est ce qu'apporte le CustomTrainingJob.

Prédéfini contre personnalisé

Vertex propose deux voies pour l'entraînement.

Les conteneurs prédéfinis sont fournis par Google, un par cadre (scikit-learn, XGBoost, TensorFlow, PyTorch) et par version. Le code d'entraînement, un simple script Python, est passé en argument. Idéal pour un prototype qui suit le rail : on ne construit pas d'image, on ne pousse rien.

Les conteneurs personnalisés sont vos images Docker, poussées dans Artifact Registry. Ils s'imposent dès que le prototype installe des dépendances non triviales, appelle des bibliothèques compilées, ou doit garantir un environnement à l'octet près entre entraînement et service. Notre projet fraude utilise XGBoost avec une extension maison de génération de variables : conteneur personnalisé obligatoire.

Construire l'image d'entraînement

Un Dockerfile minimal :

FROM europe-west1-docker.pkg.dev/vertex-ai/training/xgboost-cpu.2-1:latest

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY src/ ./src/

ENV PYTHONUNBUFFERED=1
ENTRYPOINT ["python", "-m", "src.entrainement"]

L'image de base est fournie par Vertex (xgboost-cpu.2-1) et embarque déjà google-cloud-storage, google-cloud-aiplatform et xgboost. On ajoute uniquement le supplément. Construction et publication :

gcloud builds submit \
--tag=europe-west1-docker.pkg.dev/paiement-fraude-prod/vertex/fraude-xgb:2026-09-06 \
.

gcloud builds submit construit l'image dans Cloud Build (pas sur le poste local, ce qui garantit linux/amd64 même depuis un Mac Apple Silicon) et la pousse dans Artifact Registry.

Le script d'entraînement

Vertex passe au conteneur trois variables d'environnement que le script doit connaître : AIP_MODEL_DIR (où écrire le modèle sur GCS), AIP_DATA_FORMAT et AIP_TRAINING_DATA_URI (source). Le squelette :

import os, joblib, xgboost as xgb
from google.cloud import storage
import pandas as pd
from sklearn.metrics import roc_auc_score

def charger_donnees(prefixe_gcs: str) -> tuple[pd.DataFrame, pd.DataFrame]:
client = storage.Client()
bucket_nom, prefixe = prefixe_gcs.replace("gs://", "").split("/", 1)
fichiers = [
f"gs://{bucket_nom}/{blob.name}"
for blob in client.list_blobs(bucket_nom, prefix=prefixe)
if blob.name.endswith(".parquet")
]
df = pd.concat([pd.read_parquet(f) for f in fichiers], ignore_index=True)
return df[df["split"] == "train"], df[df["split"] == "valid"]

def main():
train, valid = charger_donnees("gs://paiement-fraude-donnees/entrainement/2026-q3/")
y_train, y_valid = train.pop("est_fraude"), valid.pop("est_fraude")

modele = xgb.XGBClassifier(
n_estimators=400, max_depth=6, learning_rate=0.05,
eval_metric="aucpr", tree_method="hist",
)
modele.fit(train, y_train, eval_set=[(valid, y_valid)], verbose=False)

# Rapport la metrique pour le module 5 (Vizier).
score = roc_auc_score(y_valid, modele.predict_proba(valid)[:, 1])
print(f"aucpr_valid={score}")

# Ecriture du modele la ou Vertex l'attend.
chemin_local = "/tmp/model.joblib"
joblib.dump(modele, chemin_local)
dossier_sortie = os.environ["AIP_MODEL_DIR"]
bucket_nom, prefixe = dossier_sortie.replace("gs://", "").split("/", 1)
storage.Client().bucket(bucket_nom).blob(prefixe + "model.joblib").upload_from_filename(chemin_local)

if __name__ == "__main__":
main()

Deux détails valent d'être soulignés. Le modèle est écrit sous le nom model.joblib dans AIP_MODEL_DIR : c'est ce nom précis que le registre du module 6 attend pour un modèle scikit-learn. Et la métrique est loggée en clair (aucpr_valid=<valeur>), format que Vizier parsera au module 5.

Lancer le CustomTrainingJob

Côté carnet :

from google.cloud import aiplatform

aiplatform.init(project="paiement-fraude-prod", location="europe-west1",
staging_bucket="gs://paiement-fraude-modeles")

job = aiplatform.CustomContainerTrainingJob(
display_name="fraude-xgb-2026-09-06",
container_uri="europe-west1-docker.pkg.dev/paiement-fraude-prod/vertex/fraude-xgb:2026-09-06",
model_serving_container_image_uri="europe-docker.pkg.dev/vertex-ai/prediction/sklearn-cpu.1-3:latest",
)

modele = job.run(
replica_count=1,
machine_type="n1-standard-8",
service_account="vertex-fraude-runner@paiement-fraude-prod.iam.gserviceaccount.com",
args=["--split-date", "2026-08-15"],
base_output_dir="gs://paiement-fraude-modeles/entrainements/2026-09-06/",
sync=True,
)

Le sync=True bloque le carnet jusqu'à la fin ; sync=False rend la main immédiatement et la tâche continue en arrière-plan — préférable pour un long entraînement, on récupérera le résultat plus tard par son ID.

Choisir la famille de machine

Trois familles, trois profils de prix :

FamilleUsageExemplePrix indicatif
n1-standard-*Généraliste ancienn1-standard-8~0,38 $/h
n2-standard-*Généraliste récent, +20 % CPUn2-standard-8~0,46 $/h
c2-standard-*Calcul intensif (XGBoost, Spark)c2-standard-8~0,52 $/h
a2-highgpu-1g1 × A100 40 GoDeep learning~3,67 $/h

XGBoost tree_method=hist sature les cœurs CPU. c2-standard-16 termine notre entraînement en 22 minutes, n1-standard-8 en 55 minutes. Pour un job facturé à la seconde, la machine plus chère revient moins cher au total.

Accélérateurs, quand et lesquels

Un GPU n'est utile que si le cadre l'exploite. XGBoost n'y voit qu'un léger gain (tree_method=gpu_hist), rarement rentable au tarif. Pour un PyTorch d'apprentissage profond, la question inverse se pose : quelle carte ?

CarteMémoireCible
NVIDIA T416 GoInférence, petits entraînements
NVIDIA L424 GoGénération d'images, LLM légers
NVIDIA A100 40 Go40 GoEntraînement standard
NVIDIA A100 80 Go80 GoFondation, contextes longs
NVIDIA H10080 GoEntraînement à l'échelle

Le quota de A100 en europe-west1 est de zéro par défaut ; il faut le demander explicitement avec au moins deux semaines d'avance pour la production.

Ne pas oublier sync=False sur les longs entraînements

Un entraînement de six heures lancé en sync=True monopolise le noyau du carnet. Si la connexion Jupyter se coupe (pause déjeuner, réseau instable), la tâche Vertex continue de tourner — mais on perd la référence Python. Avec sync=False, on récupère l'ID du job et l'on ferme le carnet sans crainte. Une bonne discipline est de toujours logguer l'ID retourné.

En résumé

  • Conteneur prédéfini pour un prototype simple, personnalisé dès que l'environnement compte — c'est notre cas.
  • gcloud builds submit construit l'image dans Cloud Build et la pousse dans Artifact Registry, sans dépendre du poste.
  • Le script d'entraînement lit AIP_MODEL_DIR et écrit model.joblib sous ce chemin, format attendu par le registre.
  • CustomContainerTrainingJob prend le container_uri et un model_serving_container_image_uri distinct pour le service.
  • La machine plus chère à l'heure est souvent moins chère au total ; un GPU ne se justifie que si le cadre l'exploite.

Module suivant : nous ne lançons plus un seul entraînement mais des dizaines, avec Vizier pour trouver les meilleurs hyperparamètres.