Aller au contenu
English

Entrées et sorties des prédictions

  • Leçon 1
  • 40 min
  • Module 1 : Définir le contrat d'inférence

Ce que vous saurez faire

Notez exactement ce que le modèle d’escalade accepte et retourne, pour refuser un billet de soutien invalide avant son évaluation.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 40 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

Testé avec Python 3.14.6 sur macOS, avec une puce Apple silicon. Les étapes pour Windows et Linux n’ont pas été testées.

Dans le cours sur les API et L’essentiel de Docker et du nuage pour les développeurs en IA, vous avez créé ticket-api. Cette API attribue une catégorie à chaque billet. Dans ce cours, vous créez un deuxième petit service, escalation-service. Il répond à une question : quel est le risque d’escalade de ce nouveau billet?

Pourquoi ne pas ajouter le modèle à ticket-api? Avec un modèle par service, chaque modèle a sa propre image, ses propres mises en production et ses propres retours en arrière. Un nouveau modèle d’escalade n’exige jamais une nouvelle mise en production de l’API de catégories. Le service de soutien appelle ticket-api pour la catégorie et escalation-service pour le risque. Les données sont celles de Larkfield, utilisées dans le cours d’apprentissage automatique. Elles sont synthétiques.

Au bureau des passeports, un agent vérifie votre formulaire avant que quelqu’un le traite. Une date de naissance dans le futur ou un nom vide, et l’agent vous rend le formulaire au comptoir. Personne n’imprime un passeport à partir d’un formulaire invalide. Personne ne devine ce que vous vouliez dire.

Un modèle n’a pas d’agent. Il prend tous les nombres que vous lui donnez et retourne un score.

Priya charge le pipeline du cours d’apprentissage automatique et évalue un billet d’exemple : priorité 1, commande de 89,50 $, 112 mots. Elle remplace ensuite la priorité par des valeurs que le service de soutien n’utilise jamais : 0 et 4. Exécutez ceci dans ~/projects/ticket-model, avec son .venv activé. (Si vous avez téléchargé C17/start, il n’a pas de .venv. Exécutez ceci plus tard, après l’étape 3 de Écrire le contrat, avec le .venv de escalation-service activé.)

Python, in ~/projects/ticket-model
import joblib
import pandas as pd
pipeline = joblib.load("model/escalation_model.joblib")
ticket = dict(channel="email", team="payment", segment="home", region="west", priority=1,
order_value=89.5, word_count=112, customer_tenure_days=400,
prior_tickets_90d=2, created_hour=10, prior_escalations_90d=1)
for priority in [0, 1, 2, 3, 4]:
row = pd.DataFrame([{**ticket, "priority": priority}])
print(priority, round(pipeline.predict_proba(row)[0, 1], 4))

La sortie :

0 0.5702
1 0.3507
2 0.1802
3 0.0821
4 0.0351
  • priorités 1 à 3 : valeurs utilisées par le service de soutien
  • priorités 0 et 4 : valeurs qui n'existent pas
Le pipeline brut évalue les priorités 0 et 4 sans signaler de problème. La priorité 0 obtient le score le plus élevé et déclenche un signalement.
Description textuelle du graphique

Le billet d’exemple, évalué par le pipeline du cours d’apprentissage automatique, sans vérification préalable. La priorité 1 donne 0,351, la priorité 2 donne 0,180 et la priorité 3 donne 0,082. La priorité 0, qui n’existe pas, donne 0,570, le score le plus élevé, et le billet est signalé. La priorité 4, qui n’existe pas non plus, donne 0,035.

Aucune erreur, aucun avertissement. Le pipeline a prolongé la ligne apprise : un nombre plus bas signifie un billet plus urgent. Ainsi, 0 semble « plus urgent qu’urgent ». Priya a essayé d’autres erreurs sur le même billet :

Modification Score Ce que le pipeline a fait
created_hour 25 0,3213 A évalué une heure qui n’existe pas
word_count −50 0,1775 A évalué un compte négatif; le signalement est passé de vrai à faux
channel "Email" 0,4215 A évalué un canal jamais vu comme « aucun des canaux connus »
order_value 0 0,3240 A évalué une commande de 0 $
order_value vide (NaN) 0,3597 A inséré la médiane des données d’entraînement et activé l’indicateur « manquant »
priority "high" (élevée) erreur ValueError: Cannot use median strategy with non-numeric data (impossible d’utiliser la stratégie de médiane avec des données non numériques)

Seule la dernière erreur arrête le pipeline, et son message ne nomme même pas le champ. Toutes les autres erreurs deviennent des scores qui semblent normaux. Le service de soutien agirait en fonction de ces scores.

Utiliser un modèle entraîné pour évaluer un nouveau cas s’appelle l’inférence. L’entraînement a lieu une seule fois. L’inférence a lieu pour chaque nouveau billet, longtemps après l’entraînement, avec des entrées envoyées par un autre programme. Le service doit donc vérifier les entrées au moment de l’inférence, avant que le modèle les reçoive.

Les règles du formulaire sont imprimées dessus. Le service a besoin de la même chose : une liste écrite décrivant le billet attendu et la réponse retournée. C’est le contrat d’inférence. Les deux côtés s’y fient : le client sait quoi envoyer, et le modèle sait ce qu’il reçoit.

Pour le modèle d’escalade, le contrat précise six choses :

  1. Noms et ordre. Les 11 variables utilisées pour entraîner le pipeline, dans l’ordre de l’entraînement, sans autre clé.
  2. Types. Du texte pour les quatre catégories (channel, team, segment, region), des nombres entiers pour les comptes et les heures, et un nombre pour order_value.
  3. Plages. Par exemple, priority de 1 à 3, created_hour de 0 à 23, order_value de 0 à 5 000.
  4. Valeurs manquantes. Quelles variables peuvent être nulles, et ce que signifie une valeur nulle.
  5. Catégories inconnues. Une valeur de catégorie jamais vue par le modèle est acceptée, avec un avertissement. Le service les compte, car leur présence en grand nombre indique que les entrées ont changé (Planifier les vérifications en production dans le cours d’apprentissage automatique).
  6. La sortie. Ce que le service retourne, et ce que chaque valeur signifie.

Tomás demande : « Le predict.py du cours d’apprentissage automatique vérifiait déjà le fichier CSV. Pourquoi recommencer? » Parce que predict.py vérifiait un fichier écrit par le propre traitement de Priya. Un service reçoit des requêtes de programmes que d’autres personnes écrivent et modifient. Après une seule mise en production incorrecte du client, tous les billets peuvent avoir une priority de 0.

Un contrat a un coût. Il est strict par choix : un client avec un ancien format reçoit une erreur jusqu’à ce que son équipe le corrige. Modifier les entrées du modèle modifie maintenant le contrat, et les deux côtés doivent accepter ce changement. Pour un essai rapide dans un carnet (notebook) Jupyter, vous n’en avez pas besoin. Pour un service appelé par d’autres programmes, oui.

La règle 4 demande de l’attention. order_value peut être vide pour une vraie raison : le billet n’a pas de commande. Comparez quatre requêtes qui diffèrent sur un seul point :

La requête contient Signification Ce que dit le contrat
"order_value": 89.5 Une commande de 89,50 $ Accepté
"order_value": null Il n’y a pas de commande Accepté : le pipeline insère la médiane des données d’entraînement, comme pendant l’entraînement
"order_value": 0 Une commande de 0 $ Accepté, mais ce n’est pas la même chose qu’une absence de commande
Aucune clé order_value Le client l’a oubliée ou envoie un ancien format Rejeté

La mesure précédente montre pourquoi c’est important : une valeur nulle donne 0,3597 et 0 donne 0,3240 pour le même billet. Un client qui écrit 0 pour « aucune commande » modifie un peu chaque score, sans qu’aucune erreur apparaisse. Le contrat exige donc toutes les clés. Une valeur nulle dit « Je sais que cette valeur est vide ». Une clé absente ne dit rien, et le service ne doit pas deviner.

Seules deux variables peuvent être nulles : order_value (aucune commande) et customer_tenure_days (un invité n’a pas de première commande). Les neuf autres ne peuvent jamais être nulles.

Une réponse valide du service terminé ressemble à ceci (une réponse réelle) :

{"ticket_id":"T-120001","score":0.35065699401024064,"flag":true,"threshold":0.19,"model_version":"1.0.0","warnings":[]}

Chaque valeur a une seule signification :

  • score (score), de 0 à 1. Un score plus élevé indique un risque d’escalade plus élevé. Le score classe les billets; ce n’est pas une probabilité calibrée. Le cours d’apprentissage automatique a montré dans Seuils et calibration que ces scores ne correspondent pas aux taux réels d’escalade. Personne ne doit donc lire 0,35 comme « une probabilité de 35 % ».
  • flag (indicateur) : vrai quand le score atteint ou dépasse le seuil. Le billet va à l’équipe expérimentée.
  • threshold (seuil) : 0,19. Il appartient à la version du modèle. Il a été choisi pour que ce modèle signale environ 20 % des billets, soit la capacité de révision de Grace. Un nouveau modèle a besoin de son propre seuil.
  • model_version (version du modèle), pour relier chaque réponse au modèle qui l’a produite.
  • warnings (avertissements), par exemple une catégorie inconnue. Le score est moins certain.

Vous écrivez maintenant la partie du contrat qui concerne les entrées. Le service et ses réponses arrivent au module 3.

Créez le projet et son environnement virtuel, comme dans le cours sur les API.

  1. Créez les dossiers et allez dans le dossier du projet.

    Fenêtre de terminal
    mkdir -p ~/projects/escalation-service/escalation ~/projects/escalation-service/tests
    cd ~/projects/escalation-service
  2. Créez requirements.txt. Les quatre paquets du modèle ont les mêmes versions que dans le cours d’apprentissage automatique. Le modèle doit fonctionner avec les versions qui ont servi à l’entraîner.

    requirements.txt
    # Needed to run the service
    fastapi[standard]==0.142.2
    scikit-learn==1.9.1
    pandas==3.0.6
    numpy==2.5.3
    joblib==1.6.0
    # Needed to work on the service
    pytest==9.1.1
    httpx2==2.13.1
  3. Créez l’environnement virtuel, activez-le et installez les paquets.

    Fenêtre de terminal
    python3 -m venv .venv
    source .venv/bin/activate
    python -m pip install -r requirements.txt
  4. Créez .gitignore. Certaines lignes concernent des fichiers que les prochains modules créeront.

    .gitignore
    .venv/
    __pycache__/
    .env
    out/
    *.log
    bench/results.jsonl
  5. Créez deux fichiers vides : escalation/__init__.py et tests/__init__.py. Ils transforment les deux dossiers en paquets Python, pour que les tests puissent importer escalation.

Écrivez maintenant le contrat. Pydantic vérifie les noms, les types et les plages, comme dans le cours sur les API. La première partie du fichier énumère les variables et leurs règles :

escalation/contract.py
"""The inference contract: what a ticket must look like before the model scores it.
The model learned from 11 features. This file says, for each one, its name,
its type, the values it may take, and whether it may be missing. The API and
the batch job both check tickets with these classes, so the two paths always
apply the same rules.
"""
import math
import pandas as pd
from pydantic import BaseModel, ConfigDict, Field
# The order of the columns that the pipeline was fitted on (C05 ticket_model.py).
FEATURES = [
"channel",
"team",
"segment",
"region",
"priority",
"order_value",
"word_count",
"customer_tenure_days",
"prior_tickets_90d",
"created_hour",
"prior_escalations_90d",
]
CATEGORIES = ["channel", "team", "segment", "region"]
class TicketFeatures(BaseModel):
"""The 11 features of one ticket, as they are when the ticket is created."""
model_config = ConfigDict(extra="forbid")
channel: str = Field(min_length=1, max_length=30, description="How the customer contacted us.")
team: str = Field(min_length=1, max_length=30, description="The team that owns the ticket.")
segment: str = Field(min_length=1, max_length=30, description="The kind of customer.")
region: str = Field(min_length=1, max_length=30, description="The customer's region.")
priority: int = Field(ge=1, le=3, description="1 is the most urgent, 3 the least.")
order_value: float | None = Field(
ge=0, le=5000, description="The order's value in dollars. Null if there is no order."
)
word_count: int = Field(ge=0, le=10000, description="Words in the first message.")
customer_tenure_days: int | None = Field(
ge=0, le=20000, description="Days since the customer's first order. Null for a guest."
)
prior_tickets_90d: int = Field(ge=0, le=500, description="The customer's tickets in the last 90 days.")
created_hour: int = Field(ge=0, le=23, description="The hour the ticket was created (0 to 23).")
prior_escalations_90d: int = Field(ge=0, le=500, description="The customer's escalations in the last 90 days.")

Trois détails appliquent les règles. extra="forbid" refuse toute autre clé. Aucun champ n’a de valeur par défaut. Chaque clé est donc obligatoire, même order_value : float | None permet une valeur nulle, pas une clé absente. Les catégories sont du texte simple, pas une liste fixe, car une valeur inconnue doit passer avec un avertissement.

La deuxième partie va à la fin du même fichier. Elle ajoute l’identifiant du billet et trois fonctions auxiliaires :

escalation/contract.py (continued)
class TicketIn(TicketFeatures):
"""A ticket to score: its ID and its features."""
model_config = ConfigDict(
extra="forbid",
json_schema_extra={
"examples": [
{
"ticket_id": "T-120001",
"channel": "email",
"team": "payment",
"segment": "home",
"region": "west",
"priority": 1,
"order_value": 89.5,
"word_count": 112,
"customer_tenure_days": 400,
"prior_tickets_90d": 2,
"created_hour": 10,
"prior_escalations_90d": 1,
}
]
},
)
ticket_id: str = Field(pattern=r"^T-\d{6}$", description="The ticket's ID, for example T-120001.")
def unknown_categories(ticket: TicketFeatures, known: dict[str, list[str]]) -> list[str]:
"""Warnings for category values that the model never saw in training.
They are allowed: the one-hot encoder gives them no column, so the model
scores them as "none of the known values". Many of them mean the input
has changed, so the service reports and counts them.
"""
warnings = []
for name in CATEGORIES:
value = getattr(ticket, name)
if value not in known[name]:
warnings.append(f"{name}: {value!r} is a value the model never saw")
return warnings
def to_frame(tickets: list[TicketFeatures]) -> pd.DataFrame:
"""The tickets as a table with the training columns, in the training order.
A missing number (None) becomes NaN, so the pipeline's imputer fills it
with the training median, exactly as in training. Do not fill it here.
"""
rows = [t.model_dump(include=set(FEATURES)) for t in tickets]
frame = pd.DataFrame(rows, columns=FEATURES)
for name in FEATURES:
if name not in CATEGORIES:
frame[name] = frame[name].astype("float64")
return frame
def is_missing(value) -> bool:
return value is None or (isinstance(value, float) and math.isnan(value))

to_frame construit les colonnes dans l’ordre de FEATURES. scikit-learn vérifie aussi les noms de colonnes vus pendant l’entraînement. Un tableau avec d’autres noms échoue donc avec une erreur explicite. Un nombre manquant reste NaN, et l’imputeur du pipeline le remplace exactement comme pendant l’entraînement. Si le service le remplaçait par 0, il ferait l’erreur silencieuse du tableau précédent. is_missing est une petite fonction auxiliaire pour les prochains modules.

Lancez python dans ~/projects/escalation-service et envoyez deux erreurs à la fois :

Python, in ~/projects/escalation-service
from escalation.contract import TicketIn
good = TicketIn.model_config["json_schema_extra"]["examples"][0]
TicketIn(**{**good, "priority": "high", "created_hour": 24})

La sortie (abrégée) :

pydantic_core._pydantic_core.ValidationError: 2 validation errors for TicketIn
priority
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='high', input_type=str]
...
created_hour
Input should be less than or equal to 23 [type=less_than_equal, input_value=24, input_type=int]
...

Les deux champs sont nommés, avec la raison. Maintenant, omettez order_value :

Python (continued)
missing = {k: v for k, v in good.items() if k != "order_value"}
TicketIn(**missing)

La sortie (abrégée) :

pydantic_core._pydantic_core.ValidationError: 1 validation error for TicketIn
order_value
Field required [type=missing, input_value={'ticket_id': 'T-120001',...ior_escalations_90d': 1}, input_type=dict]
...

Enfin, un canal inconnu. Les catégories connues se trouvent dans le model_info.json du cours d’apprentissage automatique :

Python (continued)
import json
from escalation.contract import unknown_categories
known = json.load(open("../ticket-model/model/model_info.json"))["features"]["categories"]
unknown_categories(TicketIn(**{**good, "channel": "social"}), known)

La sortie :

["channel: 'social' is a value the model never saw"]

Quittez Python avec exit().

L’explorateur montre les réponses réelles du service terminé du module 3, pour une seule modification à la fois. Ce service utilise exactement le contract.py que vous venez d’écrire.

Explorer

Le contrat : accepté, accepté avec un avertissement ou rejeté?

Résultat
Accepté
Statut HTTP
200
Score
0,3507

Aucun changement : le service attribue un score au billet et répond 200. Toutes les clés sont présentes, chaque valeur a le bon type et est dans sa plage, et chaque catégorie est connue du modèle.

  • échoue : rejeté avec 422
  • avertissement : score attribué, et signalé
  • la règle qui le laisse passer
Les règles du contrat pour cette requête
RègleCe qu’elle ditRésultat
NomsSeulement ticket_id et les 11 caractéristiques. Aucune autre clé.réussit
Toutes les clés présentesUne clé absente est une erreur, même pour une valeur qui peut être nulle.réussit
TypesDu texte pour les 4 catégories, des nombres entiers pour les comptes, un nombre pour order_value.réussit
PlagesPar exemple priority de 1 à 3, created_hour de 0 à 23, order_value de 0 à 5 000.réussit
NullPermis seulement pour order_value (aucune commande) et customer_tenure_days (un invité).réussit
Catégories connuesUne valeur que le modèle n’a jamais vue est acceptée, avec un avertissement.réussit

Changement au billet d’exemple : aucun : le billet d’exemple tel quel

La réponse du service : 200

{
  "ticket_id": "T-120001",
  "score": 0.35065699401024064,
  "flag": true,
  "threshold": 0.19,
  "model_version": "1.0.0",
  "warnings": []
}

Vraies réponses du service avec le paquet escalation-1.0.0. L’identifiant de la requête change à chaque appel, donc il s’affiche comme « ... ».

Une seule modification du billet d'exemple à la fois. Le tableau indique la règle du contrat qui décide. En dessous se trouve la réponse réelle de POST /v1/score.
Description textuelle de l’explorateur

L’explorateur commence avec le billet d’exemple sans modification : toutes les règles passent, le service répond 200 et le score est 0,3507.

Les autres modifications et les réponses réelles : priority « high » (élevée) échoue à la règle de type : 422 avec fields: ["priority"] (champs : priority). created_hour 24 échoue à la règle de plage : 422, fields: ["created_hour"] (champs : created_hour). La clé order_value supprimée échoue à la règle « toutes les clés présentes » : 422, fields: ["order_value"] (champs : order_value). order_value nul passe, car une valeur nulle est permise pour cette variable : 200, score 0,3597. team nul échoue à la règle des valeurs nulles : 422, fields: ["team"] (champs : team). Un champ supplémentaire colour échoue à la règle des noms : 422, fields: ["colour"] (champs : colour). channel « social » passe toutes les règles, mais reçoit un avertissement, car le modèle n’a jamais vu ce canal : 200, score 0,4215, warnings: ["channel: 'social' is a value the model never saw"] (avertissements : channel : « social » est une valeur jamais vue par le modèle). Chaque réponse 422 contient le code invalid_ticket (billet invalide) et le message « The ticket does not match the model’s contract. » (Le billet ne respecte pas le contrat du modèle.)

Deux des sept modifications ont été acceptées : une valeur nulle là où elle est permise, et une catégorie inconnue avec avertissement. Le service a refusé les cinq autres, avec le nom du champ, avant l’exécution du modèle.

Les tests empêchent les règles de changer par accident. Le billet d’exemple va dans une fixture (données de préparation des tests). Chaque test modifie ainsi une seule chose dans une copie neuve :

tests/conftest.py
import pytest
# The example ticket of contract.py. Each test changes one thing in a copy.
TICKET = {
"ticket_id": "T-120001",
"channel": "email",
"team": "payment",
"segment": "home",
"region": "west",
"priority": 1,
"order_value": 89.5,
"word_count": 112,
"customer_tenure_days": 400,
"prior_tickets_90d": 2,
"created_hour": 10,
"prior_escalations_90d": 1,
}
@pytest.fixture
def ticket():
return dict(TICKET)
tests/test_contract.py
import pytest
from pydantic import ValidationError
from escalation.contract import FEATURES, TicketIn, to_frame, unknown_categories
def test_a_good_ticket_passes(ticket):
assert TicketIn(**ticket).priority == 1
@pytest.mark.parametrize(
"change, field",
[
({"priority": "high"}, "priority"),
({"priority": 4}, "priority"),
({"created_hour": 24}, "created_hour"),
({"word_count": -1}, "word_count"),
({"channel": ""}, "channel"),
({"team": None}, "team"),
({"colour": "red"}, "colour"),
],
)
def test_a_bad_value_is_rejected(ticket, change, field):
with pytest.raises(ValidationError) as error:
TicketIn(**{**ticket, **change})
assert field in str(error.value)
def test_null_is_allowed_only_where_the_contract_says(ticket):
TicketIn(**{**ticket, "order_value": None, "customer_tenure_days": None})
with pytest.raises(ValidationError):
TicketIn(**{**ticket, "word_count": None})
def test_a_missing_field_is_not_the_same_as_null(ticket):
del ticket["order_value"]
with pytest.raises(ValidationError):
TicketIn(**ticket)
def test_an_unknown_category_is_a_warning_not_an_error(ticket):
known = {"channel": ["email"], "team": ["payment"], "segment": ["home"], "region": ["west"]}
warnings = unknown_categories(TicketIn(**{**ticket, "channel": "social"}), known)
assert warnings == ["channel: 'social' is a value the model never saw"]
def test_the_frame_has_the_training_columns_in_order(ticket):
frame = to_frame([TicketIn(**{**ticket, "order_value": None})])
assert list(frame.columns) == FEATURES
assert frame["order_value"].isna().all() # NaN for the imputer, not 0

Exécutez les tests dans ~/projects/escalation-service, avec .venv activé :

Terminal
python -m pytest

La sortie (abrégée) :

collected 12 items
tests/test_contract.py ............ [100%]
============================== 12 passed in 0.26s ==============================

Le test paramétré compte comme sept tests, un pour chaque valeur invalide. La première exécution peut prendre quelques secondes, pendant que Python importe pandas.

L’équipe de Grace demande ce qui arrive si un client envoie priority avec la valeur 2,5 ou 2,0. Notez votre prédiction pour chaque valeur. Essayez ensuite les deux en Python avec TicketIn. Ajoutez la valeur rejetée à la liste dans test_a_bad_value_is_rejected.

Afficher la réponse

2,5 est rejeté : Input should be a valid integer, got a number with a fractional part [type=int_from_float, input_value=2.5, input_type=float] (l’entrée doit être un entier valide; le nombre reçu a une partie fractionnaire). 2,0 est accepté et devient l’entier 2, car Pydantic convertit un nombre sans partie fractionnaire. Le texte "2" est aussi accepté, pour la même raison. Cela ne pose aucun risque pour le modèle : 2,0 et 2 représentent la même priorité.

Ajoutez une ligne à la liste :

tests/test_contract.py
({"priority": 2.5}, "priority"),

python -m pytest indique alors 13 passed (13 tests réussis). La version du cours contient 12 tests du contrat. Si vous gardez cette ligne, les leçons suivantes afficheront donc un test de moins que chez vous.

Erreur courante

Une majuscule change le score

Symptôme : une nouvelle version du service de soutien envoie "channel": "Email". Le service répond 200, mais les scores sont plus élevés qu’avant, et chaque réponse contient un avertissement. Voici une réponse réelle du service terminé :

{"ticket_id":"T-120001","score":0.42145836840785755,"flag":true,"threshold":0.19,"model_version":"1.0.0","warnings":["channel: 'Email' is a value the model never saw"]}

Avec "email", le même billet obtient 0,3507.

Cause : le modèle a appris le canal email, en minuscules. Email est un texte différent. L’encodeur one-hot (encodage avec une colonne par catégorie) ne lui attribue donc aucune colonne. Le modèle lui donne un score comme s’il ne correspondait à « aucun des canaux connus ». Ce score est le même que pour social, un canal que le modèle n’a jamais vu.

Solution : le contrat a fait son travail : il a accepté le billet et signalé la valeur inconnue dans warnings (avertissements). Demandez à l’équipe cliente d’envoyer les valeurs exactes de model_info.json et de les tester. Ne changez pas la valeur dans le service (par exemple avec .lower()) sans consulter la personne responsable du modèle. Le contrat doit dire la même chose des deux côtés.

Pour un vrai service d’IA : les avertissements ne servent que si quelqu’un les lit; comptez-les donc. Une seule valeur inconnue est du bruit. Une hausse soudaine signifie qu’un client ou les données ont changé. Le module 5 ajoute ce compte aux indicateurs de santé du service.

Vérifiez votre compréhension

Un client envoie un billet sans la clé order_value. Un autre envoie order_value avec la valeur null. Pourquoi le contrat rejette-t-il le premier et accepte-t-il le second?

Afficher la réponse

Null indique que la valeur est vide de façon connue : le billet n’a pas de commande. Le pipeline traite ce cas comme pendant l’entraînement, avec la médiane d’entraînement et un indicateur de valeur manquante. Une clé absente ne dit rien : le client a peut-être oublié le champ ou envoyé un ancien format. Si le service devinait (null? 0?), le score pourrait changer sans aucune erreur : 0 donne 0,3240, tandis que null donne 0,3597. Chaque clé est donc obligatoire, et seules deux variables peuvent être null.

Vérifiez votre compréhension

Omar lit le score 0,35 dans une réponse et dit : ce billet a donc 35 % de chances d'être transféré. Que lui dites-vous?

Afficher la réponse

Le score classe les billets : un score plus élevé signifie un risque plus élevé. Il n’est pas calibré; 0,35 ne signifie donc pas une probabilité de 35 % (voir Seuils et calibration dans le cours Apprentissage automatique : du problème à un modèle fiable). Le service de soutien utilise plutôt l’indicateur. Le score atteint au moins 0,19, le seuil du modèle 1.0.0; le billet est donc envoyé à l’équipe expérimentée. Le seuil appartient à cette version du modèle.

Vérifiez votre compréhension

Faites une prédiction : un client envoie une priorité de 0. Que fait le pipeline brut, et que fait le service avec le contrat?

Afficher la réponse

Le pipeline brut lui donne un score sans signaler de problème : 0,5702 pour le billet d’exemple, le plus élevé parmi toutes les priorités. Le billet est donc signalé. Le service avec le contrat le rejette avec 422 et fields: ["priority"], car la priorité doit être de 1 à 3. Le modèle ne voit jamais le billet.

Ce cours se termine par trois études de cas. Chacune utilise un autre modèle pour traiter des données réelles, avec un contrat comme celui-ci.

Études de cas de ce cours

À la fin du cours, vous appliquez ce que vous avez appris à un projet réaliste, des données jusqu’à la recommandation. Terminez-en au moins une pour obtenir le certificat; les autres sont facultatives.

Récapitulatif

  • L’inférence consiste à utiliser un modèle entraîné sur un nouveau cas. Le modèle attribue un score à tout, même aux valeurs inexistantes : la priorité 0 a obtenu 0,5702.
  • Le contrat d’inférence définit la forme d’un billet : noms et ordre, types, plages, valeurs manquantes et catégories inconnues. Il définit aussi le sens de la réponse : un score de classement, un indicateur, le seuil, la version du modèle et les avertissements.
  • Une valeur absente n’est pas la même chose que null. Chaque clé est obligatoire, et seuls order_value et customer_tenure_days peuvent être null.
  • Une catégorie inconnue est acceptée avec un avertissement, et non refusée.
  • contract.py contient les règles, et 12 tests les font respecter.

Ensuite, vous empaquetez le modèle avec tout ce dont il a besoin pour fonctionner : son prétraitement, ce contrat, ses versions et un ensemble de cas de test fixes.

Leçon suivante : Empaqueter les dépendances du modèle