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.
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:
data/customers.csv és data/orders.csv: a
kezdőlabor tiszta adata.artifacts/split.json: az időbeli split manifestje, a V. ellenőrző pont: modell és következtetés
alfejezetből ismert cutoff és label-availability szűrés
rögzítésével.config/model_v3.json: a modellverzióhoz tartozó
explicit konfiguráció.models/model_candidate_v3.joblib: a saját gépen
betanított modelljelölt.Megfigyelési egység: egy rendelés, order_id. Kimenet:
minden teszt-rendelésre egy
-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
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
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.
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:
ahol a fájl bájtjainak sorozata, 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ó:
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.
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, testA 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 XA 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:
ahol
a modell által adott pontszám,
és
a pozitív és negatív tesztesetek száma,
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.0A 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.
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 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
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.
É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.
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.
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.
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.
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:
predict.py nem ugyanazt a feature-készletet állítja
elő, mint a tanító fit_model.py. Például az
account_age_days oszlop hiányzik, vagy a kategorikus
kódolás eltérő dummy oszlopokat ad. Diagnózis: írjuk ki
X_test.columns listáját, és hasonlítsuk össze a tanításkor
mentett oszloplistával.sha256sum models/model_candidate_v3.joblib összehasonlítása
a tanítási naplóval.python -c "import sklearn; print(sklearn.__version__)",
összevetni a tanítási környezet verziójával.order_id listáját a
split.json-ba, és hasonlítsuk össze halmazként.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.
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.
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.