32.4 VIII. ellenőrző pont: átadható modellprojekt

A VIII. ellenőrző pont azt kérdezi, hogy a projekt ténylegesen átadható-e egy másik futtatási környezetnek. Nem a fájlok megléte a kérdés, hanem az, hogy egy friss gépen ugyanaz a bemenet, kód és modell ugyanazt az ismert kontrollt állítja-e elő. Az alábbi munkamenet a Northstar késésmodellt csomagolja be, és a tanítás nélküli predikciós utat ellenőrzi.

32.4.1 Cél és konkrét helyzet

A feladat a VII. ellenőrző pont: szakosodási döntés kontrollal ellenőrző pontból ismert tabuláris Northstar-modell átadása: rendeléskor kell becsülni a 7 napot meghaladó kézbesítés valószínűségét. A modell itt logisztikus regresszió, mert CPU-s és értelmezhető; a tanítás a fejlesztőgép saját, ellenőrzött Python környezetében történik, a drága lépést nem ismételjük a konténerben.

Bemenetek:

Megfigyelési egység: egy rendelés, order_id. Kimenet: minden teszt-rendelésre egy [0,1][0,1]-beli predikciós valószínűség, a teszt-AUC, a feldolgozott sorok száma és egy futáskövető napló. Ellenőrzési feltétel: a bemeneti fájlok SHA-256 lenyomata megegyezik a manifestben rögzítettel, a tesztmátrix alakja egyezik, és a teszt-AUC a várt értéktől legfeljebb 10−610^{-6} abszolút eltéréssel tér el. A tűrés azért ilyen szigorú, mert ugyanaz a modell és ugyanaz az adat determinisztikus predikciót ad; a környezeti különbség csak lebegőpontos zajt okozhat.

Induló becslésként a modell teszt-AUC-ja a tanuló saját fit_model.py futtatásából ismert. A leckében példaként 0,73120{,}7312 szerepel, de ez szemléltető szám; a tényleges várt értéket a tanuló a saját futásból írja be. Az átadás akkor sikeres, ha a konténerben kapott AUC a várt érték tűrésén belül van.

32.4.2 Fogalmi magyarázat a hét szállítási elemről

A package (csomag) itt nem pusztán mappa, hanem telepíthető, verziózott Python-projekt. A src elrendezés elválasztja az importálandó kódot a teszt- és szkriptfájloktól, így a predikciós kód importálható a konténerben, a tesztben és a fejlesztői notebookban is. A csomag neve és függőségei a pyproject.toml-ban vannak; a telepítés az uv sync --locked paranccsal a lockfájlt használja, nem a lebegő verziótartományt. A lockfájlt a fejlesztő a saját gépén oldja fel; a konténer már nem keres új csomagverziót.

A tests (tesztek) szerepe az átadásban a szerződéses viselkedés rögzítése. A unit test (egységteszt) tiszta függvényekre vonatkozik, például a feature-számításra; az integration test (integrációs teszt) a teljes betöltés–predikció utat járja be. A konténerben futó predikció maga is egy integrációs teszt: ugyanazt a parancsot futtatja, mint a CI, így a „nálam fut” és a „másik gépen fut” közötti különbség nem marad rejtve.

A config (konfiguráció) a futtatás változóinak explicit, verziózott leírása. A cutoff, a feature-lista, a modellfájl elérési útja és a várt AUC a konfigban van, nem a kódban. Ez akkor lesz fontos, amikor a split megváltozik: új config, új modellverzió, de a kód változatlan.

A data hash (adatlenyomat) az a kriptográfiai ellenőrző összeg, amely a fájl bájtjaihoz rendel egy 256 bites értéket. Az SHA-256 függvény determinisztikus: ugyanaz a fájl minden gépen ugyanazt a 64 karakteres hexadecimális szöveget adja. A lenyomatot az előre rögzített manifest tartalma ellenőrzi:

H=SHA256(b1‖b2‖…‖bn)H = \operatorname{SHA256}(b_1 \Vert b_2 \Vert \dots \Vert b_n)

ahol bib_i a fájl bájtjainak sorozata, ‖\Vert az összefűzés. A hash csak a fájl integritását igazolja, nem a tartalom tudományos helyességét; a Northstar esetében a manifest rögzíti a seedet és a sorok számát is.

A run tracking (futáskövetés) minden futtatáshoz egyedi azonosítót és rendezett naplót rendel. A futásazonosító a bemeneti állapotból számítható:

r=SHA256(commit‖config_hash‖data_hash‖model_hash)[0:12]r = \operatorname{SHA256}(\text{commit} \Vert \text{config\_hash} \Vert \text{data\_hash} \Vert \text{model\_hash})_{[0:12]}

A napló tartalmazza a környezeti verziókat, a metrikákat és a kimeneti fájlok hash-eit. Így egy későbbi kérdésre megmondható, melyik konfiguráció melyik AUC-t produkálta.

A container (konténer) a futtatókörnyezetet csomagolja: operációs rendszer, Python-verzió, natív függőségek. A Dockerfile a lockfájllal telepíti a csomagokat, ezért a konténer és a fejlesztői környezet ugyanazt a feloldott függőséghalmazt látja. A VS Code Dev Container ugyanezt a Dockerfile-t használja interaktív munkához, a Remote SSH pedig egy másik gépen lévő projektet ér el; mindkettőnél a parancssori predikció marad az ellenőrzés alapja.

A recovery (helyreállítás) a hibás átadás utáni állapotkezelés. A konténerben futó szkript hiba esetén nullától különböző kóddal lép ki, és nem ír semmit a regisztrált champion állapotba. A korábban jóváhagyott modell a Modelljelölt, jóváhagyás és registry alfejezetből ismert registryben sértetlen marad; a hibás jelöltet a napló azonosítja. A helyreállítás tehát nem a hibás modell megjavítása, hanem a korábbi állapot megőrzése és a hiba diagnosztizálása.

32.4.3 Teljes minimális munkapélda

A projekt gyökérkönyvtárában az alábbi szerkezetet hozzuk létre:

northstar_model/
  pyproject.toml
  uv.lock                 # a fejlesztőgép által feloldott lock, nem a labor része
  Dockerfile
  src/northstar_model/
    __init__.py
    data_hash.py
    split.py
    features.py
    tracking.py
  scripts/
    fit_model.py
    predict.py
  config/
    model_v3.json
  models/
    model_candidate_v3.joblib
  data/
    customers.csv
    orders.csv
  artifacts/
    split.json

A pyproject.toml verziótartományt ad, a uv.lock a tényleges feloldást. A leckében a könyvgenerátor nem futtatta a lockolást; a tanuló a saját uv lock kimenetét használja. A függőségek a kezdőlabor alap stackjéből valók:

[project]
name = "northstar-model"
version = "0.3.0"
requires-python = ">=3.12,<3.13"
dependencies = [
  "numpy>=1.26,<2",
  "pandas>=2.2,<3",
  "scikit-learn>=1.5,<2",
  "joblib>=1.4,<2"
]

A hash-ellenőrzés tiszta függvénye:

# src/northstar_model/data_hash.py
import hashlib
from pathlib import Path

def sha256_hex(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(1 << 20), b""):
            h.update(chunk)
    return h.hexdigest()

A split a V. ellenőrző pont: modell és következtetés időhelyes protokollját kódolja: a tanításhoz csak olyan múltbeli rendelés használható, amelynek címkéje a cutoff előtt megérkezett; a teszt a cutoff utáni rendelés. A purged sávot a tanításból elhagyjuk. A függvény a split.json manifestet olvassa, és a target_late oszlopot a delivery_days > promised_days összefüggésből képezi:

# src/northstar_model/split.py
import pandas as pd

def make_time_split(frame: pd.DataFrame, cutoff: pd.Timestamp):
    label_ready = frame["label_available_at"] < cutoff
    train = frame[(frame["order_ts"] < cutoff) & label_ready].copy()
    test = frame[frame["order_ts"] >= cutoff].copy()
    train["target_late"] = (train["delivery_days"] > train["promised_days"]).astype(int)
    test["target_late"] = (test["delivery_days"] > test["promised_days"]).astype(int)
    return train, test

A feature-előállítás a rendelés pillanatában ismert oszlopokat használja. A kategorikus változókat one-hot kódolással alakítjuk numerikus mátrixszá, a kódolást a train oszlopain tanítjuk, és a tesztre alkalmazzuk. A account_age_days az order_ts és a korábban ismert signup_date különbsége:

# src/northstar_model/features.py
import pandas as pd

CATEGORICALS = ["segment", "region", "acquisition_channel"]
NUMERICALS = ["distance_km", "priority_flag", "basket_minor",
              "quantity_sum", "account_age_days"]

def build_features(df: pd.DataFrame, train_categories=None):
    df = df.copy()
    df["account_age_days"] = (df["order_ts"] - df["signup_date"]).dt.days
    X = df[NUMERICALS + CATEGORICALS].copy()
    X = pd.get_dummies(X, columns=CATEGORICALS, dtype=float)
    if train_categories is not None:
        X = X.reindex(columns=train_categories, fill_value=0.0)
    return X

A fejlesztőgépen futó fit_model.py tanít, kiszámolja a várt AUC-t, és elmenti a modellt, a teszt-sorazonosítókat és a várt értékeket. A kód a sklearn LogisticRegression osztályát használja, random_state=42 rögzítéssel. A tanítás után az AUC a teszt pozitív és negatív példáinak páronkénti konkordanciája:

AUC=1n+n−∑i:yi=1∑j:yj=0[I(si>sj)+12I(si=sj)]\mathrm{AUC} = \frac{1}{n_+ n_-} \sum_{i: y_i=1} \sum_{j: y_j=0} \left[ I(s_i > s_j) + \tfrac12 I(s_i = s_j) \right]

ahol sis_i a modell által adott pontszám, n+n_+ és n−n_- a pozitív és negatív tesztesetek száma, II pedig indikátorfüggvény. A tanuló a manifest.json-ból beírja a várt AUC-t a configba.

A konténerben futó predict.py a teljes átadási ellenőrzést végzi:

# scripts/predict.py
import json, sys
from pathlib import Path
import joblib
import pandas as pd
from sklearn.metrics import roc_auc_score
from northstar_model.data_hash import sha256_hex
from northstar_model.features import build_features
from northstar_model.split import make_time_split

def fail(msg):
    print(f"ERROR: {msg}", file=sys.stderr)
    sys.exit(1)

def main():
    cfg = json.loads(Path("config/model_v3.json").read_text())
    for key, rel in cfg["data"]["files"].items():
        actual = sha256_hex(Path(rel))
        if actual != cfg["data"]["hashes"][key]:
            fail(f"hash mismatch for {key}: {actual}")
    customers = pd.read_csv(cfg["data"]["files"]["customers"],
                            parse_dates=["signup_date"])
    orders = pd.read_csv(cfg["data"]["files"]["orders"],
                         parse_dates=["order_ts", "actual_delivery_at",
                                      "label_available_at"])
    frame = orders.merge(customers, on="customer_id", how="left")
    cutoff = pd.Timestamp(cfg["split"]["cutoff"])
    _, test = make_time_split(frame, cutoff)
    expected_rows = cfg["split"]["expected_test_rows"]
    if len(test) != expected_rows:
        fail(f"test row count {len(test)} != {expected_rows}")
    model = joblib.load(cfg["model"]["path"])
    X_test = build_features(test)
    pred = model.predict_proba(X_test)[:, 1]
    auc = roc_auc_score(test["target_late"], pred)
    expected_auc = cfg["model"]["expected_auc"]
    if abs(auc - expected_auc) > cfg["model"]["auc_tol"]:
        fail(f"AUC drift: {auc:.6f} vs {expected_auc:.6f}")
    run_id = make_run_id(cfg, model)
    out = Path("artifacts/runs") / run_id
    out.mkdir(parents=True, exist_ok=True)
    (out / "run.json").write_text(json.dumps({
        "run_id": run_id, "auc": auc, "expected_auc": expected_auc,
        "test_rows": len(test)
    }, indent=2))
    print(f"[{run_id}] data hash ok")
    print(f"[{run_id}] test rows: {len(test)}")
    print(f"[{run_id}] AUC: {auc:.6f}, expected: {expected_auc:.6f}, diff: {abs(auc-expected_auc):.6f}")

if __name__ == "__main__":
    main()

A futásazonosítót a bemeneti hash-ekből számító make_run_id a tracking.py-ban él; itt röviden csak annyi, hogy a konfig, adat- és modellhash konkatenációjának első 12 karakterét adja. A fenti kódban a make_run_id importját a tömörség kedvéért elhagytuk, a teljes változat a labban van.

A Dockerfile a lockfájlt másolja, és --locked móddal telepít:

FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --locked --no-dev --no-cache
COPY src ./src
COPY config ./config
COPY models ./models
COPY scripts ./scripts
COPY data ./data
COPY artifacts/split.json ./artifacts/
ENTRYPOINT ["python", "scripts/predict.py"]

A build és futtatás Linuxon vagy WSL-ben, a projektgyökérben:

# Feltéve, hogy a uv.lock már létezik a projektgyökérben
docker build -t northstar-model:0.3.0 .
docker run --rm northstar-model:0.3.0

A szemléltető, nem e könyvíráskor mért kimenet:

[3f9a1c2d4e] data hash ok
[3f9a1c2d4e] test rows: 780
[3f9a1c2d4e] AUC: 0.731200, expected: 0.731200, diff: 0.000000

A data és models mappák a konténer contextjébe kerülnek; a models/model_candidate_v3.joblib saját generálású fájl, a hash-ellenőrzés megerősíti, hogy ugyanazt a fájlt töltjük be, amelyet a fejlesztőgép jegyzett. Ismeretlen forrású joblib/pickle fájlt továbbra sem töltünk be.

32.4.4 Eredmény értelmezése

A sikeres futás három egymásra épülő invariánst ellenőriz. A hash-egyezés azt mutatja, hogy a bemeneti fájlok bitre azonosak a manifesttel; ez szükséges feltétele annak, hogy a split és a feature-mátrix azonos legyen. A sorok számának egyezése azt mutatja, hogy a cutoff és a label-availability szűrés ugyanazt a tesztpopulációt adja; ha a purged sávot valaki véletlenül a tesztbe sorolná, a sorok száma eltérne. Az AUC-eltérés nulla vagy a tűrés alatt van, mert a modell és a tesztadat determinisztikus; a 10−610^{-6} tűrés a lebegőpontos kerekítésre hagy mozgásteret. A statisztikai reprodukálhatóság itt erősebb, mint egy seed alapú újratanítás esetén, mert a már betanított modellt töltjük be.

A kódblokk után nem maradhat puszta szám: a diff: 0.000000 azt jelenti, hogy a konténer ugyanazt a modellt és adatot látta, tehát a korábban a VII. ellenőrző pont: szakosodási döntés kontrollal alfejezetben közölt AUC-állítás ebben az új környezetben is érvényes. Ha a diff például 0,0020{,}002 lenne, az nem véletlen, hanem szerződéssértés jele.

Az átadhatósági kapu értékeléséhez dokumentálni kell a tesztplatformot, az artefaktumok hashét és a ténylegesen lefuttatott ellenőrzéseket.

32.4.5 Feladatok

  1. Értelmezés. Miért nem elegendő csak a model_candidate_v3.joblib fájlt átmásolni a másik gépre? Nevezze meg legalább két hiányzó összetevőt, amelyek nélkül a predikció nem reprodukálható, és mindegyikhez írja le, milyen hibát okoz a hiányuk.

  2. Implementálás. Írja meg a make_run_id függvényt a src/northstar_model/tracking.py fájlban úgy, hogy a konfigurációs fájl SHA-256 lenyomata, a modellfájl lenyomata és a customers.csv+orders.csv lenyomatok vesszővel összefűzött szövege adja a bemenetet, és a kimenet az így kapott SHA-256 hexa szöveg első 12 karaktere legyen. A függvénynek str paramétereket kell elfogadnia, és a predict.py ezt hívja. Adja meg a függvény teljes kódját és egy kétlépéses kézi ellenőrzést az abc és def bemenetre.

  3. Hibadiagnózis. A konténerfutás ezt írja: AUC drift: 0.7291 vs 0.7312, és a folyamat 1-es kóddal kilép. A hash- és sorszám-ellenőrzés sikeres volt. Sorolja fel a lehetséges okokat fontossági sorrendben, és mindegyikhez adjon egy ellenőrzési parancsot vagy diagnosztikai lépést, amely eldönti, hogy az adott ok fennáll-e.

  4. Megváltozott feltétel. A projektvezető a predikciós ablakot áthelyezi: a jövőbeli használatban a cutoff 2025-06-01T00:00:00+00:00 lesz 2025-05-01 helyett. Az adatfájlok nem változnak. Mi marad változatlan, és mit kell újragenerálni vagy újratanítani? Adja meg a konfiguráció és a registry állapotok javasolt új verzióit, a Modelljelölt, jóváhagyás és registry alfejezetben tanult candidate/champion fogalmakkal.

32.4.6 Megoldások és indoklás

1. A modellfájl csak a betanított együtthatókat és a sklearn pipeline belső állapotát tartalmazza. Hiányzik a feature-előállítás konkrét kódja: ha a kategorikus oszlopok kódolása más sorrendű vagy más oszlopkészletű, akkor a predict_proba rossz oszlopokra hivatkozik, és téves vagy hibás predikciót ad. Hiányzik a split manifest: a teszt-AUC csak akkor hasonlítható össze, ha ugyanazok a rendelések alkotják a tesztet. Ha a cutoff nincs rögzítve, a V. ellenőrző pont: modell és következtetés időbeli validáció nem ismételhető, és a tesztbe kerülhetnek olyan múltbeli címkék, amelyek a gyakorlatban nem lennének ismertek. Végül a bemeneti adat verziója is hiányozhat: ha a másik gép régebbi orders.csv-t lát, a hash ellenőrzése nélkül nem derül ki.

2. A függvény:

import hashlib

def make_run_id(*hashes: str) -> str:
    payload = ",".join(hashes)
    return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:12]

Kézi ellenőrzés: make_run_id("abc", "def") esetén a szöveg "abc,def", ennek SHA-256 hexája cb8e... kezdetű; a pontos első 12 karaktert a tanuló a hashlib segítségével kiszámolja, majd ugyanazt a bemenetet kétszer futtatva ellenőrzi, hogy a kimenet determinisztikus. A teszt szempontjából az a fontos, hogy a függvény ugyanazt a sorrendet használja, mint a predict.py; ezért a hívás make_run_id(config_hash, data_hash, model_hash) alakú.

3. A lehetséges okok:

4. Az adatfájlok hash-e változatlan, tehát az adatverzió nem változik. A split manifest új generálást igényel, mert a cutoff eltolásával több múltbeli rendelés címkéje válik elérhetővé a tanításkor, és a tesztablak is bővül. A modellt újra kell tanítani az új tanítóhalmazon, ezért a modellhash is változik. A konfiguráció új verziót kap, például model_v4.json, az elvárt AUC-t az új tanításból számoljuk. A registryben az új jelölt candidate_v4 lesz; a korábbi candidate_v3 megmarad, és ha a review sikeres, a champion mutatót explicit döntéssel frissítik. Az adatból származó statisztikai állítás sem automatikusan érvényes az új cutoffra: a V. ellenőrző pont: modell és következtetés időbeli validációt az új ablakra is el kell végezni.

32.4.7 Gyakori tévedés ellenpéldával

A leggyakoribb hiba az, hogy a tanuló a Dockerfile-t, a CI-konfigot és a predikciós szkriptet feltölti, de nem futtatja, és a dokumentációban „konténerben ellenőrzött” átadást állít. A fájlok megléte nem bizonyíték. Tipikus ellenpélda: a Dockerfile FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim-mel indul, de a fejlesztő soha nem futtatott docker build-et; az első buildnél kiderül, hogy a uv.lock nincs a contextben, vagy a base image tag elavult. A helyes gyakorlat: minden új környezetet futtatással igazolunk, és a naplóba bekerül a tényleges parancs, a kimenet és a hibakód. Ebben a leckében a könyvgenerátor nem futtatta a Docker-példát; a fenti kimenet ennek megfelelően „szemléltető”, nem mért eredmény. A tanuló csak a saját, lokálisan futtatott logját nevezheti ellenőrzöttnek. Ez a megkülönböztetés a CI: kódellenőrzés és kis számítás új környezetben alfejezetből ismert: a nem futtatott példa nem válik igazolt állítássá attól, hogy formailag teljes.

Önellenőrzés: a configban minden bemenethez van hash? A split.json a cutoffot és a teszt-sorazonosítókat rögzíti? A predict.py hibás eredménynél nem ír futási naplót a champion állapotba? A modellfájl a saját fit_model.py kimenete, és a hash-e szerepel a manifestben? Ha bármelyikre nem a válasz, az átadás nem teljes.

32.4.8 Rövid összefoglaló

A VIII. ellenőrző pont a modellprojekt átadhatóságát méri: csomag, tesztek, explicit konfiguráció, adat- és modellhash, futáskövetés, konténer és hiba utáni visszaállítás együtt biztosítják, hogy egy másik környezet ugyanazt a kontrollt állítsa elő. A predikciós szkript a hasheket, a teszt sorainak számát és a várt AUC-t ellenőrzi; a tanítás a fejlesztőgépen marad. A sikeres konténerfutás után a korábbi fejezetben tanított modellnyilvántartásba vezethető a jelölt, de a nem futtatott Docker- vagy CI-példa sosem nevezhető ellenőrzöttnek. A következő alkalmazásban a Szűk keresztmetszet és mérési terv ezt a szállítási fegyelmet profilozással egészíti ki, hogy a futás erőforrásigénye is mérhető legyen.