A data scientist a Northstar labor tisztított adatelőállítását módosította. A helyi gépén a parancsok zöldek, de a távoli gépes munkamenetben, amelyet a Távoli interpreter és notebook biztonságos elérése alfejezetben állított be, más operációs rendszer és más Python-telepítés fut. A kérdés: minden commit után friss, tiszta környezetben is teljesül-e a kis adatra vonatkozó szerződés, mielőtt bárki a drága modellkísérletre költ?
A CI-ellenőrzés bemenete a repository aktuális commitja, benne a
lab/generate_data.py, a lab/quickcheck.py, a
szállított tiszta CSV-k és a manifest. Megfigyelési egység egy teljes
CI-futás; a reportban a rendelés az összegzési sor, a tétel a
join-kardinalitás sor, az ügyfél pedig a randomizációs egység marad. A
cél olyan kontrollaggregáció, amely hibás adatra nemnulla exit-kóddal és
ember által olvasható reporttal áll le.
Előzetes becslés: az alap seeddel generált tiszta adaton
2400 rendelés, 4796 tétel és 300
ügyfél várható. Minden rendelésre a tételek
line_total_minor összegének pontosan
basket_minor-ral kell egyeznie. Egész minor unitban
dolgozunk, ezért itt nincs lebegőpontos tűrés: az egyenlőség
==-tel ellenőrzendő.
A folyamatos integráció (continuous integration, CI) egy gépi szerződés: valamilyen eseményre — push, pull request, kézi indítás — friss környezetben lefuttat egy parancssort, és a teljes folyamat visszatérési kódját zöldre vagy pirosra minősíti. A CI job egy futtatási egység, a workflow több job összekötése, a runner pedig a Linux-, Windows- vagy macOS-gép, amely a jobot végrehajtja.
A zöld állapot mindig feltételes: „ez a workflow, ezen a runner-képpel, ezekkel a rögzített lépésekkel, ezzel az inputtal nem talált hibát”. Nem jelenti azt, hogy a modell minden támogatott platformon bitre azonosan reprodukálódik, hogy a statisztikai következtetés helyes, vagy hogy az üzleti cél teljesül. A Generalizáció és gyorsító választás alfejezetben látott seed-korlát itt visszaköszön: a seed ismételhetőséget adhat azonos környezetben, de nem garantál bitazonosságot eltérő könyvtár-, BLAS- vagy processz-szálazási környezetben.
A zöld CI ezért szűk, pontos mondatokat támaszt alá: például „a CSV-séma beolvasható”, „a kulcsok egyediek”, „a tételszintű pénzösszeg megegyezik a rendelésszintű kosárösszeggel”.
A labor pénzadata egész minor unit, így az ellenőrzés egzakt. Amikor a jövőben lebegőpontos metrikát hasonlítunk referenciához, abszolút és relatív tűrést együtt kell használni:
Itt
a mért érték,
a referencia,
az abszolút tűrés,
a relatív tűrés; mindkettő dimenziótlan vagy a mért mennyiség
mértékegységében értendő. Ha atol = 1e-9 és
rtol = 1e-6, akkor a 0.1 + 0.2 Pythonbeli
értékének 0.3-től való, nagyjából 5.55e-17
eltérése elfogadható, mert a felső határ
1e-9 + 1e-6 * 0.3 ≈ 3.01e-7.
A determinisztikus inputokra célszerű byte-szintű lenyomatot is rögzíteni. Az SHA-256 hash az adott fájl bájtjainak egyirányú, determinisztikus lenyomata. Ha két fájl bájtszinten azonos, a hash azonos; ha a sortörés átalakult CRLF-re, a bájtok mások, így a hash is más lesz. Ezért a hash a CI-ben jó kontroll, de nem mondja meg, hogy a különbség tartalmi hiba vagy csak platformos sortörés.
Az alábbi YAML szemléltető specifikáció, nem kipróbált futási jegyzék. A pontos parancsargumentumokat a labor projekt README-jéhez kell igazítani; a lépések szerkezete azonban átvihető bármely kis adatos repositoryba.
# .github/workflows/northstar-ci.yml
name: northstar-ci
on:
push:
paths:
- "lab/**"
- "ci/**"
- ".github/workflows/northstar-ci.yml"
pull_request:
jobs:
data-contract:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Repository kód kiadása
uses: actions/checkout@v4 # éles használatban commit-SHA pin ajánlott
- name: Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Deterministic data generation
run: python lab/generate_data.py
- name: Contract check
run: python lab/quickcheck.py
- name: Aggregation guard
run: >-
python ci/aggregate_check.py
--orders lab/orders.csv
--items lab/order_items.csv
--customers lab/customers.csv
--out artifacts/ci/report.json
- name: Report artifact
uses: actions/upload-artifact@v4
with:
name: aggregation-report
path: artifacts/ci/report.json
retention-days: 7
dependency-lock:
runs-on: ubuntu-24.04
timeout-minutes: 8
steps:
- uses: actions/checkout@v4
- name: Cache uv cache
uses: actions/cache@v4
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ hashFiles('lab/pyproject.toml', 'lab/uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-
- name: Install uv from project-pinned version
run: python -m pip install uv
- name: Environment sync from committed lock
working-directory: lab
run: uv sync --lockedA data-contract job csak a standard libraryt igénylő
generátort, kontrollt és aggregációt futtatja. A
dependency-lock job azt ellenőrzi, hogy a
uv.lock a repositoryban van-e, és a --locked
kapcsoló miatt akkor is pirosra vált, ha valaki kézzel megváltoztatta a
pyproject.toml-t anélkül, hogy a lockot is frissítette
volna. Ez szándékos: a CI nem oldhat fel csendben új verziót, mert akkor
a zöld állapot egy másik függőségi környezetet igazolna.
A cache csak gyorsító, nem ellenőrzés. A kulcs a
pyproject.toml és a uv.lock hashéből képződik,
így változáskor új cache-ág épül. A --frozen és a
--locked közötti különbségről az uv
projekt és ténylegesen rögzített függőségek alfejezetben tanultak
érvényesek: a CI-ben a --locked a megfelelő, mert azt is
ellenőrzi, hogy a lock nincs-e elavulva.
A kontrollaggregációhoz használt ci/aggregate_check.py
standard library-only változata:
#!/usr/bin/env python3
"""Northstar determinisztikus aggregáció-ellenőrzés; stdlib only."""
import argparse
import csv
import hashlib
import json
import sys
from pathlib import Path
EXPECTED = {"customers": 300, "orders": 2400, "items": 4796}
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def main() -> int:
p = argparse.ArgumentParser()
p.add_argument("--orders", type=Path, default=Path("lab/orders.csv"))
p.add_argument("--items", type=Path, default=Path("lab/order_items.csv"))
p.add_argument("--customers", type=Path, default=Path("lab/customers.csv"))
p.add_argument("--out", type=Path, default=Path("artifacts/ci/report.json"))
args = p.parse_args()
order_ids, baskets, order_customer_ids, duplicate_orders = set(), {}, set(), 0
with args.orders.open(newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
oid = row["order_id"]
if oid in order_ids:
duplicate_orders += 1
order_ids.add(oid)
baskets[oid] = int(row["basket_minor"])
order_customer_ids.add(row["customer_id"])
item_spend, item_keys, unknown_refs, duplicate_item_keys = {}, set(), 0, 0
with args.items.open(newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
key = (row["order_id"], row["line_id"])
if key in item_keys:
duplicate_item_keys += 1
item_keys.add(key)
if row["order_id"] not in baskets:
unknown_refs += 1
line_total = (
int(row["quantity"]) * int(row["unit_price_minor"])
- int(row["discount_minor"])
)
item_spend[row["order_id"]] = (
item_spend.get(row["order_id"], 0) + line_total
)
customer_ids, customer_rows = set(), 0
with args.customers.open(newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
customer_rows += 1
customer_ids.add(row["customer_id"])
mismatches = [
{"order_id": oid, "expected_minor": expected,
"actual_minor": item_spend.get(oid)}
for oid, expected in baskets.items()
if item_spend.get(oid) != expected
]
report = {
"expect_ref": EXPECTED,
"actual": {
"orders": len(order_ids),
"items": len(item_keys),
"customers": customer_rows,
},
"money_match": {
"orders_with_items": len([o for o in baskets if o in item_spend]),
"mismatch_count": len(mismatches),
"mismatch_examples": mismatches[:3],
},
"key_checks": {
"duplicate_order_ids": duplicate_orders,
"duplicate_item_keys": duplicate_item_keys,
"unknown_order_refs": unknown_refs,
"missing_customer_examples": sorted(order_customer_ids - customer_ids)[:5],
},
"input_sha256": {
"orders": sha256(args.orders),
"items": sha256(args.items),
"customers": sha256(args.customers),
},
}
ok = (
len(order_ids) == EXPECTED["orders"]
and len(item_keys) == EXPECTED["items"]
and customer_rows == EXPECTED["customers"]
and duplicate_orders == 0
and duplicate_item_keys == 0
and unknown_refs == 0
and not (order_customer_ids - customer_ids)
and len(mismatches) == 0
)
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8")
print(json.dumps(report, ensure_ascii=False, indent=2))
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())A szkript shape-ellenőrzései egyben adatminőségi kapuk: a várt sor-
és kulcsszám, a kulcs-egyediség, az idegen kulcsok létezése és a
pénzösszeg-egyezés. Az artifacts/ci/report.json csak a
reportot és a hasheket tartalmazza, sem ügyfél-szintű nyers sort, sem
titkot.
A példa ellenőrzéséhez egy háromsoros mesterséges bemenetet érdemes kézzel számolni:
order_id |
line_id |
quantity | unit_price_minor | discount_minor | line_total_minor |
|---|---|---|---|---|---|
| O1 | L1 | 2 | 100 | 0 | 200 |
| O1 | L2 | 3 | 50 | 0 | 150 |
Ha a rendelés basket_minor = 350, és
shipping_fee_minor = 90, akkor a tételszintű összeg
200 + 150 = 350, tehát a kosár-invariant teljesül. Ha
viszont valaki a szállítási díjat tételszintű join után összegzi, az
O1 kétszer szerepel a joinban, és 180-at kap
90 helyett. A Join-kardinalitás és
duplázódó célváltozó alfejezetben tanult one-to-many csapda így a
CI-ban külön diagnosztikai értékként is megjelenhet; a helyes
shipping_totals-t csak a rendelés-szemcsézettségű táblából
szabad összegezni.
A Northstar starter_experiment.py prediktív futása és az
Egyszerű előrejelző modellek és baseline
idősor-baselineok kiértékelése nem minden commitra való. A
CI-specifikációban a drága lépés legyen kézi indítású job:
training:
runs-on: ubuntu-24.04
timeout-minutes: 45
if: github.event_name == 'workflow_dispatch'
needs: [data-contract, dependency-lock]
steps:
- uses: actions/checkout@v4
- name: Full baseline experiment
working-directory: lab
run: uv run --locked python starter_experiment.py
- name: Keep only small experiment artifacts
uses: actions/upload-artifact@v4
with:
name: experiment-artifact
path: artifacts/
retention-days: 5Itt nincs valódi felhős erőforrásigény: a kiinduló labor CPU-s, kis
adatos. A workflow_dispatch miatt a drága tanítás csak
akkor fut, amikor a csapat kéri. Az artifact retention rövid, és nem a
teljes modellt, hanem a futási manifestet, split- és metrikafájlokat
célozza. A Modellartefaktum és betöltési határ
alfejezet betöltési határa érvényben marad: a CI által előállított
artifact visszatöltésénél verzió- és sémavizsgálat kell.
Értelmezés. A CI data-contract
jobja zöld. Írja le, mely állításokat támasztja alá ez a zöld állapot,
és melyeket nem. Nevezzen meg legalább két olyan következtetést, amelyet
tilos ebből levonni.
Implementálás. Bővítse a
ci/aggregate_check.py-t egy
shipping_duplication_guard riportmezővel. A kód számítsa ki
egyszer a rendelésenkénti shipping_fee_minor összeget, majd
azt a hipotetikus összeget, amelyet az order_items
tételszámával megszorzott szállítási díjak adnának. Mindkét értéket írja
a report-ba shipping_guard kulcs alatt. A repo
a Northstar tiszta CSV-it használja; nem kell valódi felhőt
hívni.
Hibadiagnózis. A dependency-lock
job a uv sync --locked lépésnél piros lesz: „lockfile does
not exist”. A fejlesztő erre a --frozen-re cserélné a
parancsot, hogy a CI zöld legyen. Mondja meg, miért rossz ez a
diagnózis, és mi lenne a helyes javítás.
Megváltozott feltétel. A csapat úgy dönt, hogy a
prediktív baseline-t hetente egyszer futtatja a CI-ben, de továbbra sem
minden commitra. Írja át a workflow-t úgy, hogy a training
job ütemezett cron ütemben is induljon, és a
data-contract job továbbra is minden pushra fusson. Az
artifact megőrzési idő maradjon 5 nap.
1. A zöld data-contract job azt
igazolja, hogy az adott runner-képen, az adott commitban a generátor
lefutott, a quickcheck szerződése teljesült, a CSV-k beolvashatók
voltak, a várt sor- és kulcsszámok egyeztek, és a tételszintű pénzösszeg
minden beolvasott rendelésre megegyezett a basket_minor
értékkel. Nem igazolja, hogy a modell jövőbeli predikciója pontos, hogy
a kísérleti ITT-hatás konfidenciaintervalluma minden új mintán fed, vagy
hogy a workflow egy másik platformon is bitre azonos modellt hozna
létre. A zöld állapot csak a definiált adatszerződést ellenőrizte.
2. A reportba illeszthető kontroll minimális kiegészítése:
correct_shipping = 0
hypothetical_shipping = 0
with args.orders.open(newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
count = 0
for item_row in item_count_by_order.get(row["order_id"], []):
count = item_count_by_order[row["order_id"]]
correct_shipping += int(row["shipping_fee_minor"])
hypothetical_shipping += count * int(row["shipping_fee_minor"])A megoldásnak pontos adatszerkezetet kell adnia:
item_count_by_order[oid] az order_id-hoz
tartozó line_id-k száma. Az elvárt invariant
hypothetical_shipping >= correct_shipping, és a
különbség sum((item_count - 1) * shipping_fee_minor). A
kontroll nem hibát dob feltétlenül, hanem a reportban láthatóvá teszi a
kardinalitási csapda lehetséges mértékét. Ellenőrzése a belinkelt tiszta
CSV-k hashelt bemenetén, megegyező műveleti sorrend mellett
reprodukálható; lebegőpontos tűrésre itt nincs szükség, mert a pénz
egész cent.
3. A --frozen kihagyja a lock
aktualitásvizsgálatát, tehát a CI egy olyan lockot fogadna el, amely nem
tükrözi a pyproject.toml-t. Ezzel nem oldódik meg a hiány
vagy az elavulás, csak elrejtjük a piros jelzést. A helyes javítás: a
fejlesztő a lokális projektben futtatja az uv lock
parancsot, a keletkezett uv.lock-ot commitolja, majd a CI-t
újra futtatja. A cache jelenléte itt irreleváns, mert a cache tárolt
csomagokat, nem lockállományt.
4. A training job indítási feltétele
legyen:
on:
push:
paths: [...]
pull_request:
schedule:
- cron: "30 4 * * 1"
workflow_dispatch:A training jobban a feltétel:
if: github.event_name == 'workflow_dispatch' ||
github.event_name == 'schedule'A data-contract job marad push-onként. A pontos
cron kifejezést a repository UTC-szabályaihoz kell
igazítani; az itteni 30 4 * * 1 hetfőn 04:30 UTC-t jelöl. A
cron ütemezés a platform szolgáltatása, ezért a pontos időzítés a
szolgáltató fiókbeállításaitól függhet.
Tévedés: „A CI zöld lett, tehát a modell a célplatformon is reprodukálódik.” Ellenpélda: a CI Linux-ubuntu runneren futtatott egy szkicit-learn pipeline-t, és az artifactot egy Windows-os gép tölti vissza. A modell float-számításai a NumPy verziójától, a BLAS-tól és a szálak összegzési sorrendjétől függően kicsit eltérhetnek. A CI nem futtatta a tényleges célplatformot, így a bitazonosságot nem is mérte. Hasonló korlátot mutatott a Generalizáció és gyorsító választás alfejezet a seed-ek platformfüggőségénél és a Modellartefaktum és betöltési határ az artifact-visszatöltésnél: a zöld pipeline nem menti fel a csapatot a betöltéskor futtatott séma- és predikciós szerződésvizsgálat alól.
data-contract job timeouttal, tiszta Ubuntu-képpel és
standard library-only lépésekkel fut.dependency-lock job a --locked kapcsolót
használja, a cache pedig a pyproject.toml és a
uv.lock hashére épül.A CI zöld státusza nem bizonyítja, hogy a modell statisztikailag megfelelő; a tesztek csak a kifejezetten megfogalmazott szerződéseket ellenőrzik.
A CI a repository minden pushját friss, tiszta Linux-környezetben
futtatott kis adatos szerződésként ellenőrzi. A Northstar-példában a
generátor, a quickcheck és a kontrollaggregáció egy gyors standard
library-only jobban fut: kulcsegyediséget, várható sor- és tételszámot,
valamint tételszintű pénzösszeg-egyezést ellenőriz. A függőségek
szinkronizálása uv sync --locked módon történik, a cache
csak gyorsít. A drága modelltanítás kézi vagy ütemezett külön job, rövid
artifact retentionnel. A zöld CI szűk, feltételes állítás, nem teljes
platformos modellreprodukció. A következő lépésben a VIII. ellenőrző
pont ezekből az elemekből állít össze átadható modellprojektet,
beleértve a betöltési határokat és a futási manifestet.