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 :
| Famille | Usage | Exemple | Prix indicatif |
|---|---|---|---|
n1-standard-* | Généraliste ancien | n1-standard-8 | ~0,38 $/h |
n2-standard-* | Généraliste récent, +20 % CPU | n2-standard-8 | ~0,46 $/h |
c2-standard-* | Calcul intensif (XGBoost, Spark) | c2-standard-8 | ~0,52 $/h |
a2-highgpu-1g | 1 × A100 40 Go | Deep 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 ?
| Carte | Mémoire | Cible |
|---|---|---|
NVIDIA T4 | 16 Go | Inférence, petits entraînements |
NVIDIA L4 | 24 Go | Génération d'images, LLM légers |
NVIDIA A100 40 Go | 40 Go | Entraînement standard |
NVIDIA A100 80 Go | 80 Go | Fondation, contextes longs |
NVIDIA H100 | 80 Go | Entraî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.
sync=False sur les longs entraînementsUn 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 submitconstruit l'image dans Cloud Build et la pousse dans Artifact Registry, sans dépendre du poste.- Le script d'entraînement lit
AIP_MODEL_DIRet écritmodel.joblibsous ce chemin, format attendu par le registre. CustomContainerTrainingJobprend lecontainer_uriet unmodel_serving_container_image_uridistinct 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.