A Northstar predikciós projektben a korábbi egységekben felépítettünk
egy időhelyes splitet (Feldolgozási gráf és
idempotencia), egy predikciós szerződést (Predikciós szerződés és hibateszt) és egy
küszöbválasztási protokollt (Küszöb, kalibráció és
ritka esemény). Most arra a kérdésre válaszolunk: hogyan kötjük a
kutatási állítást a tényleges futáshoz? A konkrét feladat az, hogy a
starter_experiment.py két baseline-futását – egy konstans
(tanítóarányos) és egy distance_km küszöbös baseline-t –
úgy dokumentáljuk, hogy később bárki meg tudja mondani, melyik kód,
melyik adat, melyik paraméter és melyik környezet állította elő az adott
metrikát.
A bemenet a lab artifacts/split.json és a
feature-fájlok, a megfigyelési egység a rendelés, a célváltozó a
target_late indikátor. A manifest célja: minden futáshoz
rögzíteni a paramétereket, a metrikákat, a bemeneti fájlok hashét, az
artefaktumokat és a futtató környezetet. Előzetes becslésként a konstans
baseline log loss értéke nagyjából a tanítóbeli pozitív arányból adódó
–
körüli érték; a distance baseline attól való eltérése a kutatási kérdés,
nem előre ismert eredmény.
A futás (run) egyetlen végrehajtás, amelynek
rögzített a kódja, a bemeneti adata, a paraméterei és a kimeneti
artefaktumai. A kísérlet (experiment) olyan futások
célzott halmaza, amelyek ugyanarra a kérdésre válaszolnak, és csak a
tervezett paraméterekben térnek el egymástól. A kettő közötti különbség
nem technikai bürokrácia, hanem a kutatási napló szerkezete: ha ma csak
két baseline-t futtatsz, a kísérlet neve lehet
northstar-late-baselines-v1; ha holnap egy modellcsaládot
is hozzáadsz, az új kísérlet lesz.
A futásazonosító (run ID) két részből áll: egy ember által olvasható névből és egy rövid hashből. A hash fontos tervezési döntése, hogy mit tartalmaz. A metrikákat nem szabad beletenni, mert azokat a futás után számítjuk ki, és a futásnak már akkor azonosíthatónak kell lennie, amikor az értékelés még nem fejeződött be. Ezért a hash az úgynevezett identity materialból készül: a paraméterek kanonikus JSON-sorrendbe rendezett reprezentációjából és a bemeneti fájlok hashéből. A metrika később hozzáadódhat a manifesthez anélkül, hogy a futás identitása megváltozna.
A paraméterek (parameters) a futás előtt ismert, a
kód által vezérelt beállítások: például a strategy, a
cutoff vagy a distance küszöb. A seed is paraméter. A
bemeneti adatfájl viszont nem paraméter, hanem input: az
input_hash mező azonosítja. Ez a különbségtétel azért
fontos, mert ha a split-fájl megváltozik, a manifest új futást igényel,
akkor is, ha a paraméterek azonosak.
A metrikák (metrics) futás utáni, az értékelési
protokollt követő skalár összegzések. A metrika csak a definíciójával
együtt értelmezhető, ezért a manifestben a
metric_definitions mezőt is rögzítjük. A korábbi
egységekben használt két metrika:
ahol
a megfigyelt címke,
a prediktált valószínűség az
-edik
tesztsorban,
a tesztsorok száma, és mindkét kimenet
skalár. A Brier-pontszám mindig
-ben
van, a log loss viszont elvileg nem korlátos, amikor a predikció a
megfigyeléssel ellentétes szélére kerül; a gyakorlati implementáció
levágást használ. A metric_definitions rögzíti a pozitív
osztályt (target_late == 1), a küszöböt és az
értékét is.
Az artefaktumok (artifacts) a futás által kiírt fájlok: a predikciós CSV, a betanított pipeline, esetleg egy ábra. A manifest nem tartalmazza magukat a fájlokat, csak a nevüket és a tartalomhashüket. Az artefaktumot immutábilisnak tekintjük; ha újra akarjuk futtatni, új manifest készül, nem felülírjuk a régit.
A környezet (environment) leírása három szinten fontos. A numerikus reprodukálhatóság az azonos seedet és a lebegőpontos műveletek azonosságát jelenti; a statisztikai reprodukálhatóság ugyanazt az eloszlásból vett becslést; a környezeti reprodukálhatóság a Python-verziót, a platformot, a zárolt függőségeket és a Git-revíziót rögzíti. A manifest mindig a harmadik szintet írja le, ebből a másik kettő nem következik automatikusan.
MLflow, DVC vagy hash-alapú manifest? A hash-alapú JSON-manifest kis lokális projektekben elég, és semmilyen szolgáltatást nem igényel. Az MLflow ugyanezt a fogalmat szolgálja ki paraméter-, metrika- és artefaktumlogolással, lokális mappába is; a DVC elsősorban a nagy adatfájlok tartalomcímzését és tárolását végzi. A főút ebben a fejezetben a manifest; az MLflow-t opcionális rétegként mutatjuk, de a keretjegyzet alapján a telepített verzió viselkedését külön ellenőrizni kell.
A következő példa standard libraryt használ, így a manifest fogalma
csomagtelepítés nélkül ellenőrizhető. Az adatok szándékosan kicsik: négy
tesztsorral mutatjuk be, hogy két baseline ugyanolyan accuracy-t adhat,
miközben a log loss nagyságrendileg különbözik. A valós labban ugyanez a
funkció a lab/pyproject.toml numerikus csomagjaival fut; a
fájlneveket a lab dokumentációja adja.
import csv, hashlib, json, math, pathlib, platform, sys, datetime
EPS = 1e-12
def clip_log_loss(y_true, p_pred):
p = [max(EPS, min(1 - EPS, v)) for v in p_pred]
return -sum(y * math.log(pi) + (1 - y) * math.log(1 - pi)
for y, pi in zip(y_true, p)) / len(y_true)
def brier(y_true, p_pred):
return sum((pi - y) ** 2 for pi, y in zip(p_pred, y_true)) / len(y_true)
def accuracy(y_true, p_pred):
return sum((1 if pi >= 0.5 else 0) == y for pi, y in zip(p_pred, y_true)) / len(y_true)
def sha256_text(s):
return hashlib.sha256(s.encode("utf-8")).hexdigest()
# kis példaadatok
train_y = [0, 1, 0, 1, 1, 0]
test_y = [1, 0, 0, 1]
test_ids = ["T7", "T8", "T9", "T10"]
distance_km = [70, 45, 90, 30]
cutoff = "2025-05-01T00:00:00+00:00"
positive_class = "target_late == 1"
def make_predictions(strategy):
if strategy == "constant":
return [sum(train_y) / len(train_y)] * len(test_y)
if strategy == "distance":
threshold = 60.0
return [1.0 if d > threshold else 0.0 for d in distance_km]
raise ValueError(f"unknown strategy {strategy}")
def build_manifest(strategy, preds, artifact_name, artifact_hash):
input_source = {
"train_y": train_y,
"test_y": test_y,
"distance_km": distance_km,
"cutoff": cutoff,
}
input_hash = sha256_text(json.dumps(input_source, sort_keys=True))
params = {"strategy": strategy}
if strategy == "distance":
params["threshold"] = 60.0
identity_material = f"{json.dumps(params, sort_keys=True)}|{input_hash}"
short_hash = sha256_text(identity_material)[:8]
run_id = f"baseline_{strategy}_{short_hash}"
metrics = {
"log_loss": clip_log_loss(test_y, preds),
"brier": brier(test_y, preds),
"accuracy": accuracy(test_y, preds),
}
return {
"run_id": run_id,
"experiment": "northstar-late-baselines-v1",
"created_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"parameters": params,
"metrics": metrics,
"input_hash": input_hash,
"artifacts": {
"predictions_csv": artifact_name,
"predictions_csv_sha256": artifact_hash,
},
"metric_definitions": {
"log_loss": "-mean(y*log(p)+(1-y)*log(1-p)), y in {0,1}, p in (0,1), eps=1e-12",
"brier": "mean((p-y)^2)",
"accuracy": "mean(1[p>=0.5]==y)",
"positive_class": positive_class,
},
"environment": {
"python": sys.version.split()[0],
"platform": platform.platform(),
"lab_requires": "lab/pyproject.toml",
"git_rev": "UNCOMMITTED-placeholder",
},
}
def save_predictions(path, y_true, preds):
with open(path, "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["row_id", "y_true", "p_late"])
for row_id, y, p in zip(test_ids, y_true, preds):
w.writerow([row_id, y, p])
def main():
out_dir = pathlib.Path("runs")
out_dir.mkdir(exist_ok=True)
manifests = []
for strategy in ("constant", "distance"):
preds = make_predictions(strategy)
artifact_name = f"preds_{strategy}.csv"
artifact_path = out_dir / artifact_name
save_predictions(artifact_path, test_y, preds)
artifact_hash = sha256_text(artifact_path.read_text(encoding="utf-8"))
manifest = build_manifest(strategy, preds, artifact_name, artifact_hash)
manifest_path = out_dir / f"{manifest['run_id']}.json"
manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
manifests.append(manifest)
print(json.dumps(manifests, indent=2))
if __name__ == "__main__":
main()A fenti kódot a kapcsolódó példaprojektben még nem ellenőriztük; az alábbi kimenet ezért szemléltető, a kód logikájából számított érték. Az olvasó saját gépén a futás a Python 3.12 szemantikája szerint ugyanezt adja, feltéve, hogy a példaadatok azonosak. A manifest egyes mezői így nézhetnek ki a konstans baseline-ra:
"run_id": "baseline_constant_4e17c2a1",
"parameters": {"strategy": "constant"},
"metrics": {"log_loss": 0.6931, "brier": 0.25, "accuracy": 0.5},
"metrics": {"log_loss": 13.8155, "brier": 0.5, "accuracy": 0.5}A distance baseline-nál a log loss a várt módon magas, mert a példa
tesztkészletén a küszöbös predikció az ellenkező címkét kapja meg. A
run_id rövid hashje a paraméterekből és az input hashből
adódik, nem a metrikákból; ezért a metrikák későbbi pontosítása nem
változtatja meg a futás azonosítóját.
Kézi kontroll: esetén minden tesztsor tagja , így a konstans log loss , a Brier pedig . A distance baseline esetén a predikciók , a megfigyelések , ezért a 8. és a 10. sorban a modell -t vagy -et mond a tényleges ellenkezőjére; a levágás után a log loss . Mindkét baseline accuracy értéke , tehát az accuracy önmagában nem tudja megkülönböztetni a kettőt; a manifestben rögzített log loss és Brier viszont igen.
Invariáns ellenőrzés: minden metrika a tesztsorokra számítva
skalár; a Brier mindig
-ben
van; a run_id utolsó 8 karaktere megegyezik a
identity_material SHA-256 hashének első 8 karakterével. Ha
a split-fájl egy bájtja megváltozik, az input_hash
megváltozik, és a run_id is új futást jelöl.
Az MLflow opcionális rétegként ugyanezt a manifestet írhatja egy
helyi mlruns mappába:
# opcionális, nem ellenőrzött ebben a kéziratban
import mlflow
mlflow.set_tracking_uri("file://./mlruns")
mlflow.set_experiment("northstar-late-baselines-v1")
with mlflow.start_run(run_name=manifest["run_id"]):
mlflow.log_params(manifest["parameters"])
mlflow.log_metrics(manifest["metrics"])
mlflow.log_artifact(str(artifact_path))A pontos mlflow API-viselkedés a telepített verziótól
függ; ha az olvasó használni akarja, a
lab/pyproject.toml-ben szereplő függőségek és a
forrásjegyzék mlflow kulcsa alapján ellenőrizze. A manifest
e nélkül is teljes.
Értelmezés. Egy manifest csak a következő mezőket tartalmazza:
{"metrics": {"log_loss": 0.42}, "parameters": {"strategy": "distance"}}Miért nem lehet eldönteni, hogy a 0.42 jó-e? Sorolj fel
legalább négy hiányzó mezőt, és magyarázd el mindegyik szerepét egy-egy
mondatban.
Megvalósítás. Egészítsd ki a példakódot egy
priority baseline-nal: rögzített paraméterekkel
,
ha a sor priority_flag=1, és
,
ha
priority_flag=0$. A példa tesztadataihoz add meg apriority_flag
= [1, 0, 1,
1]oszlopot. Számítsd ki a három metrikát, és írd be a manifestet aruns/`
mappába.
Hibadiagnózis. A manifestben a
run_id értéke baseline_constant_a1b2c3d4, de
amikor a tanuló újraszámítja az identity_material hashét,
az e9f8a7b6-ot ad. Két lehetséges okot is talál a kódban:
az egyik a paraméterdict sorrendje, a másik a metrikák beemelése az
anyagba. Melyik a helyes diagnózis, és hogyan javítod a kódot?
Megváltozott feltétel. A lab kutatási kérdése a
cutoffot 2025-05-01-ről 2025-06-01-re helyezi.
Mely manifestmezők változnak biztosan, és mi a helyes eljárás a régi
futás manifestjével? Mondd meg, miért nem elég a régi metrikát
felülírni.
A manifest hiányos. Szükséges legalább: input_hash,
amely a split és a feature-fájlok tartalmát azonosítja;
metric_definitions, amely a log loss képletét, a pozitív
osztályt és az eps levágást rögzíti; environment, amely a
Python- és csomagverziókat adja; artifacts, amely a
predikciós CSV hashét hordozza; és
experiment/run_id, amely a futást a kísérleti
táblázatba köti. A 0.42 önmagában csak egy szám: ha a
tesztkészlet más, a metrika összehasonlíthatatlan; ha a definíció más, a
jelentése más.
Az új predikció tömböt a make_predictions-höz
hasonlóan állítjuk elő:
priority_flag = [1, 0, 1, 1]
def make_priority_predictions():
return [0.7 if f == 1 else 0.2 for f in priority_flag]A metrikák: a tesztsorok
címkéivel a log loss a fenti levezetésből
,
a Brier
,
az accuracy
.
A manifest parameters mezője
{"strategy": "priority", "p_flag_1": 0.7, "p_flag_0": 0.2},
az input hash az új oszloppal újra számítandó. Ellenőrzés: a
run_id hashje megváltozik a konstanshoz képest; a CSV
artefaktumnak is új hashje van.
A helyes diagnózis a metrikák beemelése. Ha az
identity_material a teljes manifestből készül, a metrikák
utólagos módosítása megváltoztatja a run_id-t, ami
ellentmond a futás stabil azonosíthatóságának. A paraméterdict sorrendje
önmagában is okozhat eltérést, ezért sort_keys=True kell;
de a metrikák kizárása a tervezési hiba. A javítás: az anyag csak
parameters és input_hash legyen, ahogy a
példakód teszi.
Változnak: parameters["cutoff"], az
input_hash (az új split miatt), a run_id
hashje, a metrikák, az artefaktumok hashjei, és a
created_at új időpontot kap. A helyes eljárás: a régi
manifestet nem módosítjuk; a kísérlet új futást kap, és a kísérleti
táblázat appendel, nem update-el. A régi metrika felülírása azért hibás,
mert a futás immutábilis bizonyíték: ha valaki a régi állítást akarja
reprodukálni, látnia kell, milyen splittel készült. A kísérleti naplóban
a két cutoff két külön sort jelent, és az összehasonlítás csak ezt a
különbséget kezelve érvényes.
„A logolt metrika önmagában összehasonlítható.” Ellenpélda: a fenti példában a konstans és a distance baseline accuracy értéke egyaránt . Ha a manifest csak az accuracy-t tartalmazná, a kutatás azt hinné, hogy a két baseline egyforma; a log loss értéke illetve viszont megmutatja, hogy a distance baseline a kis példán katasztrofálisan rossz. A metrika csak az értékelési protokollal – split azonosító, küszöb, pozitív osztály, definíció – együtt értelmezhető. Ugyanez vonatkozik az MLflow-logolásra is: az MLflow UI-ban látott szám a manifest mögötti szerződés nélkül ugyanolyan puszta szám.
Ellenőrizd, hogy a manifestben a run_id hashje a
paraméterekből és az input hashből készül, nem a metrikából; minden
artefaktum rendelkezik hashsel; a metric_definitions
tartalmazza a pozitív osztályt; a környezetben van Python-verzió és
lab_requires hivatkozás; és a kísérleti táblázatban minden
futás külön sor. Ha ezek közül bármelyik hiányzik, a reprodukálhatósági
állítást szűkítsd, és ne nevezd a futást teljesen dokumentáltnak.
A példabeli futásazonosító a paraméterekből és a bemeneti hashből képzett rövid, determinisztikus hash. Az azonos paraméterű és bemenetű ismétlések ugyanazt a logikai azonosítót kapják: egyedi futáspéldányhoz külön időbélyeg vagy UUID szükséges. A nyolc hexadecimális jegy ütközésmentességet nem garantál; a metrikák később adódnak hozzá, ezért nem részei az identitásnak. A manifest rögzíti a paramétereket, a metrikákat a definícióikkal, az artefaktumok hashét és a futtató környezetet, így a kutatási állítást a tényleges kódhoz és adathoz köti. A kis házilagos JSON-manifest lokális baseline-okhoz elég; az MLflow ugyanerre a fogalomra ad kényelmesebb felületet, de a szám mögötti protokoll akkor is kötelező. Következő alkalmazás: Adatverzió és splitmanifest, ahol az adat- és splitmanifest verziózása teszi ezt a futásazonosítást nagyobb projektekben is használhatóvá.