31.1 Futásazonosító és kísérleti táblázat

31.1.1 Cél és konkrét helyzet

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ó 0.680.68–0.720.72 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.

31.1.2 Fogalmi magyarázat: futás, kísérlet és azonosítás

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:

Llog=−1N∑i=1N[yilogpi+(1−yi)log(1−pi)],B=1N∑i=1N(pi−yi)2, L_{\mathrm{log}} = -\frac{1}{N}\sum_{i=1}^{N}\left[y_i\log p_i + (1-y_i)\log(1-p_i)\right], \qquad B = \frac{1}{N}\sum_{i=1}^{N}(p_i-y_i)^2,

ahol yi∈{0,1}y_i\in\{0,1\} a megfigyelt címke, pi∈(0,1)p_i\in(0,1) a prediktált valószínűség az ii-edik tesztsorban, NN a tesztsorok száma, és mindkét kimenet (1,)(1,) skalár. A Brier-pontszám mindig [0,1][0,1]-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ó ε=10−12\varepsilon=10^{-12} 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 ε\varepsilon é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.

31.1.3 Teljes minimális munkapélda

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.

31.1.4 Eredmény értelmezése

Kézi kontroll: p=0.5p=0.5 esetén minden tesztsor tagja −log0.5=0.6931-\log 0.5 = 0.6931, így a konstans log loss 0.69310.6931, a Brier pedig (0.5−0)2=(0.5−1)2=0.25(0.5-0)^2=(0.5-1)^2=0.25. A distance baseline esetén a predikciók [1,0,1,0][1,0,1,0], a megfigyelések [1,0,0,1][1,0,0,1], ezért a 8. és a 10. sorban a modell p=0p=0-t vagy p=1p=1-et mond a tényleges ellenkezőjére; a levágás után a log loss 14(10−12+27.63+10−12+27.63)≈13.82\frac{1}{4}(10^{-12} + 27.63 + 10^{-12} + 27.63)\approx 13.82. Mindkét baseline accuracy értéke 0.50.5, 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 (1,)(1,) skalár; a Brier mindig [0,1][0,1]-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.

31.1.5 Feladatok

  1. É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.

  2. Megvalósítás. Egészítsd ki a példakódot egy priority baseline-nal: rögzített paraméterekkel p=0.7p=0.7, ha a sor priority_flag=1, és p=0.2p=0.2, 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.

  3. 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?

  4. 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.

31.1.6 Megoldások és indoklás

  1. 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.

  2. 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 [1,0,0,1][1,0,0,1] címkéivel a log loss a fenti levezetésből 0.5350.535, a Brier 0.1780.178, az accuracy 0.50.5. 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.

  3. 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.

  4. 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.

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

„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 0.50.5. Ha a manifest csak az accuracy-t tartalmazná, a kutatás azt hinné, hogy a két baseline egyforma; a log loss értéke 0.6930.693 illetve 13.8213.82 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.

31.1.8 Önellenőrzés

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.

31.1.9 Rövid összefoglaló

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á.