1.3 Reprodukálhatóság és tudományos munkanapló

1.3.1 Cél és konkrét helyzet

A Northstar tanulóprojektben a lab/generate_data.py állítja elő a szintetikus adatokat. Az előző alfejezetben rögzítettük, hogy egy mező mikor válik ismertté, és hogy a nyers adat, a tiszta adat és a manifest együtt adja a származási láncot. Most az a kérdés: ugyanaz a generátorfolyamat két külön mappában, ugyanazzal a seeddel vajon bájtról bájtra ugyanazt a CSV-t adja-e, és mi történik, ha pontosan egy bemeneti paramétert, a seedet változtatjuk meg?

A bemenet itt maga a generátorkód, a környezet és a paraméterezés; a megfigyelési egység egy-egy kimeneti CSV-fájl bájttartalma; az eredmény e fájlok SHA-256 lenyomata, valamint a futásról készített jegyzék. Előzetes becslés: ha a generátor determinisztikus, az azonos seeddel készült customers.csv, orders.csv, order_items.csv és orders_raw.csv hash-ei páronként azonosak lesznek. Ha csak a seedet változtatjuk, legalább a tartalmi hash-ek eltérnek, még akkor is, ha a fájlok neve és szerkezete nem változik.

1.3.2 Determinizmus, véletlen és a seed szerepe

Egy lépést determinisztikusnak nevezünk, ha ugyanaz a bemenet és ugyanaz a programállapot mindig ugyanazt a kimenetet adja. Ilyen például egy CSV-sor összeállítása, egy fájl kiírása vagy egy hash kiszámítása. Ezzel szemben egy sztochasztikus lépés egy véletlen forrás állapotát is felhasználja: a kimenet a lehetséges értékek egy halmazából kerül ki, és a lépés megismétlése önmagában nem garantálja ugyanazt az eredményt.

A számítógépes szimulációban a „véletlen” általában pszeudovéletlen: egy determinisztikus függvény állítja elő a sorozatot, de a sorozat úgy viselkedik, mintha véletlen húzásokból állna. A seed (mag) ennek a függvénynek a kezdeti állapotát rögzíti. Ugyanaz a seed ezért ugyanazt a pszeudovéletlen sorozatot választja; más seed más sorozatot indít. A seed rögzítése a sztochasztikus lépést gyakorlati értelemben ismételhetővé teszi, de nem szünteti meg a mögöttes kísérleti bizonytalanságot: a későbbi statisztikai fejezetekben például egy becsült hatás a minta véletlene miatt akkor is ingadozik, ha a szimuláció seedje rögzített.

Kis kézi példa egy egyszerű lineáris kongruens generátorral:

xn+1=(5xn+1)mod16, x_{n+1} = (5 x_n + 1) \bmod 16,

ahol xnx_n a nn-edik állapot, értéke a {0,1,…,15}\{0,1,\ldots,15\} halmazból való, n=0,1,2,…n = 0,1,2,\ldots indexű, a seed pedig x0x_0. Ha x0=3x_0=3, a sorozat:

3,0,1,6,15,12,13,2,11,8,9,14,7,4,5,10,3,… 3, 0, 1, 6, 15, 12, 13, 2, 11, 8, 9, 14, 7, 4, 5, 10, 3, \ldots

Ugyanezt a számolást bárki megismétli, és pontosan ugyanezt kapja. Ha viszont x0=1x_0=1, a sorozat 1,6,15,12,…1,6,15,12,\ldots, tehát más. Ez a példa nem ipari minőségű véletlenszám-generátor, de pontosan mutatja a seed szerepét: egy rövid állapotból hosszú, előre meghatározott sorozat indul.

A fájlok összehasonlításához kriptográfiai hash-t, például SHA-256-ot használunk. A hash egy determinisztikus függvény:

h:{0,1}*→{0,1}256, h : \{0,1\}^* \to \{0,1\}^{256},

ahol {0,1}*\{0,1\}^* tetszőleges hosszúságú bájt-sorozat, {0,1}256\{0,1\}^{256} pedig a 256 bites kimenetek halmaza. Azonos bemenetre azonos kimenetet ad; ha két fájl hash-e eltér, akkor a fájlok biztosan eltérnek. A hash-összehasonlítás gyakorlati ellenőrzésre való: egyezésnél nem bizonyítja matematikai értelemben, hogy a két fájl azonos, de a gyakorlatban ez az elfogadott ujjlenyomat-módszer. A Northstar manifest.json ugyanezt az elvet használja: seedet, sorszámokat, hash-eket és kontrollösszegeket rögzít.

1.3.3 Futási jegyzék és hipotézisnapló

A futási jegyzék (run log) a ténylegesen elvégzett lépések időrendi feljegyzése. Egy sor akkor hasznos reprodukáláshoz, ha tartalmazza:

A kontrollált változtatás (controlled change) azt jelenti, hogy két futás között pontosan egy tényezőt változtatunk, a többit rögzítjük. Ha két dolgot egyszerre változtatunk, nem tudjuk eldönteni, melyik okozta az eltérést. A Northstar-generátornál a bemeneti tényezők közé tartozik legalább a seed, a customers és az orders paraméter. A tanulási feladat ezért először csak a seedet változtatja, a többi paramétert változatlanul hagyja.

A hipotézisnapló a döntést megelőző várakozást és a döntést követő ellenőrzést kapcsolja össze. Minden sorhoz három elem tartozik:

A futási jegyzék a múltat rögzíti, a hipotézisnapló a várakozás és az eredmény közötti kapcsolatot teszi láthatóvá. Ez a kettő együtt adja a tudományos munkanapló legfontosabb részét. A naplóba az 1.2-ben megismert módon érdemes azt is feljegyezni, hogy egy adott döntés pillanatában mely adatmezők voltak ismertek: a reprodukálhatóság nemcsak a számolásé, hanem az információ időbeli rendjéé is.

1.3.4 Teljes minimális munkapélda

A következő munkamenetet a tanuló gépén kell futtatni; a könyv szövegének írásakor ezt a kódot nem futtattuk, ezért a megjelenő kimenet elméletileg várt, nem mért. A lab/generate_data.py pontos kapcsolóit a helyi --help kimenete mutatja; ha a kapcsoló neve eltér, a döntési logika ugyanez.

Első lépésként hozzunk létre két kimeneti mappát, és futtassuk ugyanazt a generátort ugyanazzal a seeddel.

# Linux/macOS, a könyv projektgyökeréből
mkdir -p runs/run_a runs/run_b
python lab/generate_data.py --seed 20261020 --customers 300 --orders 2400 --output-dir runs/run_a
python lab/generate_data.py --seed 20261020 --customers 300 --orders 2400 --output-dir runs/run_b

Windows PowerShellben ugyanez a két parancs mkdir runs\run_a, runs\run_b előkészítés után, a --output-dir kapcsolóval. A fájlok összehasonlítását platformfüggetlen Python-szkripttel végezzük, mert így a könyv fő eszközkészlete is gyakorolható:

# compare_hashes.py
import hashlib
import sys
from pathlib import Path

FILES = ["customers.csv", "orders.csv", "order_items.csv", "orders_raw.csv"]

def sha256(path: Path) -> str:
    h = hashlib.sha256()
    h.update(path.read_bytes())
    return h.hexdigest()

def main(left: Path, right: Path) -> int:
    failed = False
    for name in FILES:
        a = sha256(left / name)
        b = sha256(right / name)
        status = "egyezik" if a == b else "ELTÉR"
        print(f"{name:20s} {status}  {a[:16]}  {b[:16]}")
        failed = failed or (a != b)
    return 1 if failed else 0

if __name__ == "__main__":
    sys.exit(main(Path(sys.argv[1]), Path(sys.argv[2])))

A futtatás:

python compare_hashes.py runs/run_a runs/run_b

Elméletileg várt kimenet: mind a négy CSV esetén egyezik állapot és azonos hash-előtag, a folyamat kilépési kódja 0.

Ezután hajtsuk végre a kontrollált változtatást: csak a seedet írjuk át 20261021-re, minden más paramétert megtartva.

python lab/generate_data.py --seed 20261021 --customers 300 --orders 2400 --output-dir runs/run_c
python compare_hashes.py runs/run_a runs/run_c

Itt az elméletileg várt eredmény az, hogy a négy CSV hash-e eltér. A customers=300 és orders=2400 paraméter megmarad, ezért a vásárlók és rendelések sorainak száma lehet azonos, de a tartalom más; az order_items.csv sorainak száma is változhat, mert az alapméret csak a seed=20261020 futásra érvényes. A pontos ellenőrzési alap minden futásnál a saját manifest.json, nem az alapkiadás adatai.

A futási jegyzék formátuma például ilyen lehet; a cellák tartalmát a saját futásodból kell kitöltened:

Idő Parancs és input Megfigyelt eredmény Döntés
1. generate_data.py, seed=20261020, output run_a CSV-k és manifest létrejöttek elfogadva
2. ugyanaz seed=20261020, output run_b mind a négy CSV hash egyezik determinisztikusnak tekintjük
3. ugyanaz seed=20261021, output run_c CSV hash-ek eltérnek run_a-tól seed kontroll alatt tart

A hozzá tartozó hipotézisnapló első három sora:

  1. Hipotézis: a generátor azonos paraméterekkel bitazonos CSV-t ad. Alátámasztó jel: a run_a és run_b hash-ei egyeznek. Cáfoló jel: bármelyik hash eltér. Időpont és döntés: az run_b futás után ellenőrizve; a generátort ezen a környezeten reprodukálhatónak tekintjük.
  2. Hipotézis: a seed része a bemenetnek. Alátámasztó jel: run_c hash-e eltér run_a-tól. Cáfoló jel: minden hash azonos marad. Időpont és döntés: a run_c futás után ellenőrizve; a seedet minden jövőbeli futásnál rögzítjük.
  3. Hipotézis: a manifest a futásra jellemző ellenőrző összeget ad. Alátámasztó jel: a manifestek hash- vagy kontrollösszeg-mezői a saját futás CSV-ihez illeszkednek. Cáfoló jel: a manifest más seedet vagy hash-t mutat, mint a fájlok. Időpont és döntés: minden átadás előtt ellenőrizzük.

1.3.5 Az eredmény értelmezése

A hash-egyezésnél nincs tolerancia: a cél a bájtazonosság, tehát egyetlen eltérő bájt is ELTÉR állapotot ad. A kézi kontroll az LCG-példa: x0=3x_0=3 esetén a 16 hosszú periódus újra és újra ugyanúgy ismétlődik, tehát a seed rögzítése a sorozatot is rögzíti. A CSV-k esetében ez azt jelenti, hogy az azonos seed nemcsak hasonló, hanem azonos bájtsorozatot eredményez, feltéve hogy a generátorkód, a Python-verzió, a paraméterek és a fájlkiírási sorrend is azonos.

A shape- és dtype-ellenőrzés a hash-nél gyengébb, de fontos kiegészítő invariáns: az alap seeddel customers.csv 300 sort, orders.csv 2400 sort, order_items.csv 4796 sort tartalmazzon. Ez önmagában nem bizonyítja a tartalom azonosságát, ezért a manifest vagy a hash a döntő. A más seeddel készült run_c esetén a shape-egyezés nem is elvárás; a generált adat szerződése szerint az alapméretek csak az alap seedhez tartoznak.

A reprodukálhatóság itt numerikus és fájlszintű. A későbbi gépi tanulási futásoknál más lesz a helyzet: ott a seed rögzítése a véletlen műveletek sorrendjét rögzíti, de a lebegőpontos műveletek hardverfüggő sorrendje miatt a bitazonosság nem garantálható. Ezt a különbséget a fejezet végén visszahozzuk.

1.3.6 Feladatok

  1. Értelmezés. A run_a és run_b mind a négy CSV hash-e egyezik. Mondd el saját szóval, mit állíthatunk ebből a generátorról, és mit nem állíthatunk, ha a második futás egy másik gépen, más Python-verzióval történne.

  2. Megvalósítás. Egészítsd ki a compare_hashes.py-t úgy, hogy a CSV-k mellett a két mappa manifest.json fájlját is beolvassa, és akkor is 1 kilépési kóddal térjen vissza, ha a manifestekben a seed vagy bármely CSV-hez tartozó hash mező eltér. A kilépési kód legyen a hívó számára géppel ellenőrizhető.

  3. Hibadiagnózis. Azt látod, hogy a run_c/manifest.json seed mezője 20261021, a compare_hashes.py runs/run_a runs/run_c mégis minden CSV-re egyezést ír ki. Adj két lehetséges magyarázatot, és mindkettőhöz egy-egy konkrét ellenőrző lépést.

  4. Kontrollált változtatás továbbvitele. Tartsd a seedet 20261020-on, de változtasd a customers paramétert 300-ról 301-re, az orders paramétert hagyd 2400-on. Mely kimeneti fájlokban vársz változást, és milyen ellenőrzéssel döntöd el, hogy a változás valóban a customers paramétertől származik?

  5. Hipotézisnapló. Írj be a naplóba egy negyedik hipotézist arról, hogy a generátor kimenete függ-e a fájlkiírás előtti munkakönyvtár nevétől. Add meg az alátámasztó és cáfoló megfigyelést, valamint a mérési tervet.

1.3.7 Megoldások és indoklás

  1. Az egyezésből azt állíthatjuk, hogy az adott gépen, az adott Python-verzióval és az adott paraméterekkel a generátor két futtatása azonos CSV-ket adott. Nem állíthatjuk, hogy más gépen vagy más Python-verzióval is bitazonos lesz: a generátor használhat olyan standard könyvtári részeket, amelyek viselkedése verzióhoz kötött, és a fájlrendszer, a karakterkódolás vagy a lebegőpontos formázás is különbözhet. A környezet ugyanúgy része a reprodukálási feltételnek, mint a seed.

  2. A manifest.json beolvasása után például ellenőrizheted, hogy a két seed mező egyezik-e, majd a CSV-nevekhez tartozó hash-eket összevetheted. Az invariáns: a kilépési kód 0 pontosan akkor, ha minden összehasonlított mező egyezik. A hash-összevetésnél nincs tűréshatár; a szöveges mezőknél a pontos egyezést kérjük.

  3. Két lehetséges magyarázat:

  4. A customers paraméter az ügyfélfájlt közvetlenül érinti, ezért customers.csv sorszáma 300-ról 301-re változik. Az orders.csv is változhat, mert az ügyfélazonosítók halmaza más lesz; az order_items.csv és az orders_raw.csv szintén változhat az előállítási lánc miatt. A kontrolláltság abban áll, hogy a seedet és az orders paramétert rögzítjük, így ha a run_a-hoz képest valamelyik fájl hash-e eltér, az a customers változtatás következménye. Az ellenőrzés alapja a run_301 saját manifest.json, nem az alapkiadás méretei.

  5. Például: „A generátor által kiírt CSV-k tartalma nem függ a kimeneti mappa nevétől.” Alátámasztó jel: runs/run_a és egy eltérő nevű, például runs/other_name mappába írt azonos seedű futás hash-ei egyeznek. Cáfoló jel: bármelyik hash eltér. Mérési terv: két új mappa létrehozása, azonos seed, hash-összevetés, az eredmény bejegyzése a futási jegyzékbe.

1.3.8 Gyakori tévedés

„Elég egy seedet rögzíteni, és a gépi tanulási futás minden gépen bitazonos lesz.” Ez nem igaz. A seed a pszeudovéletlen műveletek sorrendjét rögzíti, például azt, hogy melyik minták kerülnek egy-egy kötegbe. A hardver azonban a lebegőpontos összeadás sorrendjét párhuzamosításkor megváltoztathatja: egy GPU redukciója másként csoportosíthatja az összeadandókat, mint egy CPU, és a lebegőpontos kerekítés sorrendfüggő. Ettől a veszteség vagy a súlyok utolsó tizedesjegyei eltérhetnek, noha a véletlen döntések ugyanazok voltak. A tanulási példánkban a CSV-generátor nem használ ilyen párhuzamos lebegőpontos redukciót, ezért fájlszintű bitazonosságot várhatunk; ML-futásnál a reprodukálhatóság szintje külön vizsgálandó.

Egy másik félreértés: a seed egyezése adatminőségi garanciát is jelentene. A orders_raw.csv ugyanazzal a seeddel is tartalmazza a szándékos hibákat, mert azok a generátor specifikációjának részei. A seed csak az előállítás reprodukálhatóságát szolgálja; a tisztaságot külön ellenőrzéssel kell igazolni.

1.3.9 Önellenőrzés

Kérdezd végig magadon a friss munkanaplóddal kapcsolatban: le tudom-e írni a generátorfutás pontos parancsát, seedjét és paramétereit? A jegyzékemben külön szerepel-e a tervezett lépés és a ténylegesen mért kimenet? Minden döntésemhez tartozik-e előre rögzített alátámasztó és cáfoló megfigyelés? Meg tudom-e nevezni, hogy a fájlszintű reprodukálhatóság mely feltételei hiányoznak egy másik gépre költözéskor? Ha bármelyik kérdésre nincs válasz, a naplót egészítsd ki, mielőtt a következő diagnózisfeladathoz kezdesz.

Reprodukálhatóság szintjei. A rögzített véletlen mag nem garantál minden operációs rendszeren vagy könyvtárverzión bájtazonos eredményt. Külön kell dokumentálni (1) a forrásadat és a konfiguráció bájtazonosságát, (2) az eredmények számszerű egyezését deklarált tolerancián belül és (3) a következtetés robusztusságát. Mindháromhoz más elfogadási feltétel tartozik.

A sha256 kizárólag azonos fájlbájtokat ellenőriz; nem bizonyítja, hogy az adatok helyesek vagy az elemzés tudományosan érvényes. A munkanapló tartalmazza a Python- és csomagverziókat, a platformot, az inputazonosítót, az elemzőkód commitját és a ténylegesen ellenőrzött kimenetet.

1.3.10 Rövid összefoglaló

A reprodukálhatóság nem egyetlen seed, hanem a kódverzió, környezet, paraméterek, kimeneti adatverzió és a döntések együttes rögzítése. A determinisztikus lépés azonos bemenetből azonos kimenetet ad; a sztochasztikus lépést a seed rögzítése teszi újrafuttathatóvá. A Northstar-generátorral azonos seed mellett bájtazonos CSV-k várhatók, más seed mellett eltérő hash-ek. A futási jegyzék a múltat, a hipotézisnapló a várakozás és a mérés kapcsolatát rögzíti; a hash-ellenőrzésnél nincs tolerancia, az ML-futásoknál a bitazonosság nem általános garancia. A következő alfejezetben ezt a naplózási fegyelmet használjuk egy teljes diagnosztikai munkamenet felépítésére.