9.1 Jelölésből végrehajtható számítás

9.1.1 Cél és konkrét helyzet

A II. ellenőrző pont után egy modellező öt rendelésre hibajelentést készített. A megfigyelési egység most egy rendelés. A bemenet egy ötelemű hibavektor:

e=(2.0,−1.0,3.0,−2.0,0.0),e = (2.0,\,-1.0,\,3.0,\,-2.0,\,0.0),

ahol minden érték mértékegysége nap, és ei=yi−ŷie_i = y_i - \hat{y}_i: a tényleges és az előrejelzett késés különbsége. A cél az átlagos négyzetes hiba (mean squared error, MSE) kiszámítása kézzel, Python-ciklussal és NumPy-val. Az ellenőrzési feltétel: a három út azonos eredményt ad, és az eredmény mértékegysége nap2\mathrm{nap}^2, nem nap\mathrm{nap}.

Mielőtt a levezetést elolvassa, adjon előzetes becslést. Az abszolút hibák átlaga 1.61.6 nap, ezért a négyzetes hiba 3–4 nap2\mathrm{nap}^2 közé eshet; a pontos kézi összeg hamarosan következik.

Az alábbi öt szám pedagógiai példa, nem a Northstar-projekt tényleges modelljének mért kimenete. A számítások viszont teljes egészében ellenőrizhetők.

Az alfejezet csak annyi matematikát tételez fel, amennyit a ndarray, shape, axis és dtype bevezetett: egy vektor itt rendezett számlista, elemei koordináták, a Python-lista eleme a NumPy-tömb egy dimenziójának felel meg. Az összeadás, szorzás és hatványozás alapművelet.

9.1.2 Szumma és index: a képlet ciklussá fordul

A −8,4,10,−5-8, 4, 10, -5 típusú hosszú összegek helyett a matematika egy tömör jelet használ. A

S=∑i=1neiS = \sum_{i=1}^{n} e_i

jelentése: „az e1,e2,…,ene_1, e_2, \ldots, e_n értékek összege”. A jelölések:

Az alsó határ i=1i=1, a felső határ nn. A Python-listák viszont 00-tól indexelnek, ezért a fordításnál ezt a különbséget mindig jelölni kell:

errors = [2.0, -1.0, 3.0, -2.0, 0.0]  # nap
n = len(errors)

s_math = 0.0
for i in range(1, n + 1):      # matematikai index: 1, 2, ..., n
    s_math += errors[i - 1]    # Python-index: 0, 1, ..., n-1

Ugyanez a beépített sum függvénnyel:

s_math2 = sum(errors)

Az öt hibára S=2+(−1)+3+(−2)+0=2.0S = 2 + (-1) + 3 + (-2) + 0 = 2.0 nap. Figyeljen arra, hogy a szumma maga lehet kicsi, miközben a tagok abszolút értéke nagy: a pozitív és negatív hibák kioltják egymást. Ezért lesz a modellértékelésben fontos a négyzetre emelés.

Kézzel ellenőrzött lépések:

ii eie_i (nap) ei2e_i^2 (nap2\mathrm{nap}^2)
1 2.0 4.0
2 -1.0 1.0
3 3.0 9.0
4 -2.0 4.0
5 0.0 0.0

A négyzetek összege 18.0nap218.0\ \mathrm{nap}^2. Az átlagos négyzetes hiba képlete tehát

MSE=1n∑i=1nei2, \mathrm{MSE} = \frac{1}{n}\sum_{i=1}^{n} e_i^2,

ahol n=5n = 5, így MSE=18.0/5=3.6nap2\mathrm{MSE} = 18.0 / 5 = 3.6\ \mathrm{nap}^2.

Egy fontos zárójel-szabály már itt látszik: ∑i=1nei2\sum_{i=1}^{n} e_i^2 a négyzetek összege, nem az összeg négyzete. Az összeg itt 2.02.0, annak négyzete 4.04.0; a négyzetek összege viszont 18.018.0. A képletbe írt hatvány a tagokra, nem az egész szummára vonatkozik.

9.1.3 Függvény, paraméter és értelmezési tartomány

A matematikai függvény (function) olyan szabály, amely egy bemenethez pontosan egy kimenetet rendel. A q(e)=e2q(e) = e^2 függvény az ee hibához a négyzetes hibát rendeli. Itt:

A paraméter (parameter) olyan mennyiség, amelyet egy adott számításon belül rögzítünk, de egy másik számításnál megváltoztathatunk. A Northstar-adatokból ismert tételszintű pénzösszeg jó példa:

L(q;p)=p⋅q, L(q; p) = p \cdot q,

ahol qq a darabszám, pp az egységár centben rögzített paramétere, az eredmény pedig a tétel értéke centben. A pontosvesszős írásmód jelzi, hogy qq a változó, pp pedig paraméter. Ugyanez Pythonban:

def line_total(quantity: int, unit_price_minor: int) -> int:
    return unit_price_minor * quantity

Ha a termék ára p=250p = 250 cent, akkor L(3;250)=750L(3; 250) = 750 cent. A függvény képlete ugyanaz marad, a paraméter értéke azonban termékenként cserélhető. A data scientist akkor dolgozik pontosan, ha meg tudja mondani, melyik szimbólum változó, melyik paraméter, és milyen tartományból vehet fel értéket. A teljes tételösszeghez az II. ellenőrző pont: adatból ellenőrzött feature-tábla szerződése még levonja a tételszintű kedvezményt: line_total_minor = quantity * unit_price_minor - discount_minor.

A tartomány nem üres formalitás. Az MSE képlet n=0n=0-ra nem értelmezett, mert nullával osztana. A Python-függvénynek ezt explicit hibaüzenettel kell jeleznie:

def mse(errors: list[float]) -> float:
    if not errors:
        raise ValueError("n must be at least 1")
    sq_sum = 0.0
    for e in errors:
        sq_sum += e * e
    return sq_sum / len(errors)

A if not errors ellenőrzés a tartományi feltétel kódba írt változata.

9.1.4 Mértékegység és tömbalak

A képlet megértésének része, hogy minden tag egységét nyomon követjük. A példa hibavektorában:

A mértékegység-ellenőrzés megelőzi a leggyakoribb félreértést: a 3.63.6 nem azt jelenti, hogy a modell átlagosan 3.63.6 nappal téved. Az átlagos abszolút hiba itt (2+1+3+2+0)/5=1.6(2+1+3+2+0)/5 = 1.6 nap, az RMSE pedig 3.6≈1.897\sqrt{3.6} \approx 1.897 nap. A 3.6nap23.6\ \mathrm{nap}^2 az MSE, tehát négyzetes skálán összegzi a hibát.

Az ábra a négyzetre emelést mutatja a példa értékein. A negatív előjel elveszik, ezért a −2-2 és 22 hiba ugyanakkora, 4nap24\ \mathrm{nap}^2 négyzetes büntetést ad:

Hibatag négyzete a hibavektor elemein - Számított függvényértékek: x^2 a 2, -1, 3, -2, 0 hibákon. Nem mérés.

Ugyanezt az ábrát interaktív notebookban egy Matplotlib scatter hívással lehetne megrajzolni; itt a könyv beépített chart-formátuma adja a pontos, ellenőrizhető pontokat.

9.1.5 Három számítási út: kéz, ciklus, NumPy

A teljes minimális munkapélda Python-fájlként futtatható. A kódot a projektgyökérben létrehozott mse_demo.py névvel mentse, majd futtassa Linux/macOS shellben python mse_demo.py, Windows PowerShellben pedig py mse_demo.py paranccsal. A kód nem használ véletlenszámot, ezért seed nem kell; Python 3.12-vel és a kezdőlabor NumPy-verziójával futtatható, APIszempontból nem verzióérzékeny:

import math
import numpy as np

errors = [2.0, -1.0, 3.0, -2.0, 0.0]  # nap
n = len(errors)

# 1) Ciklusos fordítás: a matematikai i = 1..n index Pythonban 0..n-1.
sq_sum_loop = 0.0
for i in range(n):
    e_i = errors[i]
    sq_sum_loop += e_i * e_i
mse_loop = sq_sum_loop / n             # nap^2
rmse_loop = math.sqrt(mse_loop)        # nap

# 2) NumPy vektorizált alak
errors_np = np.array(errors, dtype=float)
mse_numpy = np.mean(errors_np ** 2)

print("n =", n)
print("sq_sum_loop =", sq_sum_loop)
print("mse_loop =", mse_loop)
print("mse_numpy =", mse_numpy)
print("rmse_loop =", rmse_loop)
print("shape =", errors_np.shape, "dtype =", errors_np.dtype)

Az alábbi kimenet elméletileg várt, kézzel levezetett érték; a kód teljes futtatását külön példaprojektben ellenőrizzük. A helyi futtatásnak 10−1210^{-12}-es tolerancián belül egyeznie kell:

n = 5
sq_sum_loop = 18.0
mse_loop = 3.6
mse_numpy = 3.6
rmse_loop = 1.8973665961010275
shape = (5,) dtype = float64

A sq_sum_loop = 18.0 pontosan egyezik a kézi táblázattal. A mse_loop és mse_numpy egyezését ne ==-vel ellenőrizzük bitmintára, hanem toleranciával: abs(mse_loop - mse_numpy) < 1e-12. A 3.6 ugyanis kettes számrendszerben nem véges tört, ezért a lebegőpontos ábrázolás a legközelebbi kettes értéket tárolja; numerikus összehasonlításhoz tűrés kell.

9.1.6 Eredmény értelmezése

Az eredmény három úton ellenőrzött:

  1. A kézi összeg 4+1+9+4+0=184+1+9+4+0 = 18, tehát MSE=18/5=3.6\mathrm{MSE} = 18/5 = 3.6.
  2. A Python-ciklus ugyanezt, float típusú 18.0 részösszeggel adja.
  3. A NumPy az errors_np ** 2 vektorművelettel elemenként négyzetre emel, majd a mean a 18/518/5 normalizálást végzi.

Az ellenőrző invariantok: az elemszám n=5n = 5 megegyezik az errors_np.shape[0] értékkel; az errors_np.dtype numerikus, float64; a négyzetes hiba mértékegysége nap2\mathrm{nap}^2, az RMSE mértékegysége nap. A hibaösszeg önmaga nem elég: a 18.018.0 csak a 18/518/5 lépéssel válik átlagos négyzetes hibává. Ha a cél az eredeti hibaskálára való visszatérés, akkor az RMSE-t kell közölni, nem az MSE-t.

A kapott 3.63.6 ezen az öt rendelésen egy leíró szám, nem statisztikai becslés bizonytalansággal. Azt állítja, hogy ezen az öt megfigyelésen a négyzetes hibák átlaga 3.6nap23.6\ \mathrm{nap}^2; nem állítja, hogy egy jövőbeli rendelés hibája pontosan ekkora lesz.

9.1.7 Feladatok

  1. Értelmezés. Egy kolléga azt írja a jelentésbe: „A modell átlagosan 3.6 nappal téved.” A hibavektor továbbra is [2.0, -1.0, 3.0, -2.0, 0.0] nap. Mi a hiba a mondatban? Adja meg a helyes értelmezést, és számítsa ki az átlagos abszolút hibát, valamint az RMSE-t üzenet-ellenőrzésként.

  2. Megvalósítás. Írjon mse(errors) nevű Python-függvényt, amely üres listánál ValueError kivételt dob, egyébként ciklussal kiszámítja az MSE-t. Ellenőrizze az alábbi inputra: [2.0, -1.0, 3.0, -2.0, 0.0]. A megoldásban adjon assert ellenőrzést is.

  3. Hibadiagnózis. Az alábbi kód miért nem adja a fenti 3.6 eredményt?

errors = [2.0, -1.0, 3.0, -2.0, 0.0]
sq_sum = 0.0
for i in range(1, 6):
    sq_sum += errors[i] * errors[i]
mse = sq_sum / 5

Mi a pontos hibaüzenet vagy hibás érték oka, és hogyan kell javítani a ciklushatárt?

  1. Megváltoztatott feltétel: mértékegység-átváltás. Ugyanezeket az eltéréseket órában tárolják: [48.0, -24.0, 72.0, -48.0, 0.0] óra. Számítsa ki az MSE-t órában. Ellenőrizze, hogy az eredmény a napban kapott 3.6nap23.6\ \mathrm{nap}^2 érték 1nap2=576óra21\ \mathrm{nap}^2 = 576\ \mathrm{óra}^2 átváltásával egyezik. Miért hibás ilyenkor a nyers 3.6 érték továbbítása mértékegység nélkül?

9.1.8 Megoldások és indoklás

  1. A kolléga a nap2\mathrm{nap}^2 mértékegységet napként olvasta. A helyes állítás: az MSE 3.6nap23.6\ \mathrm{nap}^2, ami négyzetes skálán bünteti a hibát. Az átlagos abszolút hiba (mean absolute error, MAE) (2+1+3+2+0)/5=1.6(2+1+3+2+0)/5 = 1.6 nap; az RMSE 3.6≈1.897\sqrt{3.6} \approx 1.897 nap. Ellenőrzés: a MAE és az RMSE egyaránt nap mértékegységű, az MSE pedig nap2\mathrm{nap}^2, így a három szám nem felcserélhető.

  2. Lehetséges megoldás:

def mse(errors: list[float]) -> float:
    if not errors:
        raise ValueError("n must be at least 1")
    sq_sum = 0.0
    for e in errors:
        sq_sum += e * e
    return sq_sum / len(errors)

expected = mse([2.0, -1.0, 3.0, -2.0, 0.0])
assert abs(expected - 3.6) < 1e-12

Az üres lista az n=0n = 0 tiltott tartomány, ezért nem hibaüzenet nélküli ZeroDivisionError-ba kell futnia, hanem érthető ValueError kell. Az assert a 10−1210^{-12} toleranciás ellenőrzést végzi.

  1. A Python-listák indexe 00-tól indul, ezért a range(1, 6) először kihagyja a 2.0 elemet, majd az i = 5 értéknél errors[5] hozzáféréssel IndexError hibát vált ki. A javított ciklushatár range(len(errors)), vagy közvetlenül az elemeken lehet iterálni:
sq_sum = 0.0
for e in errors:
    sq_sum += e * e
mse = sq_sum / len(errors)

Ellenőrző invariant: a sq_sum összegnek 18.018.0-t kell adnia, és len(errors) értéke 55.

  1. Az órában adott hibák négyzetei: 2304,576,5184,2304,02304, 576, 5184, 2304, 0; összegük 10368óra210368\ \mathrm{óra}^2; öttel osztva 2073.6óra22073.6\ \mathrm{óra}^2. Az átváltás: 3.6⋅576=2073.63.6 \cdot 576 = 2073.6. A nyers 3.6 nem hordozza az egységet, és ha valaki napként értelmezi, miközben a tömb órában van, hibás következtetést kap. Az RMSE ellenőrzése: 2073.6≈45.537\sqrt{2073.6} \approx 45.537 óra, ami 45.537/24≈1.89745.537/24 \approx 1.897 nap, tehát visszaadja az eredeti RMSE-t.

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

„A képletet beírtam a kódba, tehát jó” — ez akkor hibás, ha a tagok egysége és a műveletek dimenziója tisztázatlan. Tipikus ellenpélda a szumma és a hatvány sorrendjének felcserélése:

# Hibás: ez az összeg négyzete ötödökkel, nem az átlagos négyzetes hiba.
s = sum([2.0, -1.0, 3.0, -2.0, 0.0])
mse_wrong = (s ** 2) / 5      # 0.8

A helyes képlet előbb elemenként négyzetre emel: sum(e*e for e in errors) / 5 = 3.6. A 0.80.8 számértéke is eltér, és a mértékegysége is baj: az összeg négyzete nap2\mathrm{nap}^2 ugyan, de nem az egyedi hibák négyzetes büntetését átlagolja. Hasonló csapda az II. ellenőrző pont: adatból ellenőrzött feature-tábla kontrollösszegeinél: a basket_minor szumma centben adódik, nem euróban; a cent/euró skálafaktor nélkül a kontrollérték üres szám.

Visszautalás: a ndarray, shape, axis és dtype alfejezetben bevezetett shape és dtype itt az ellenőrzés eszköze lett. A (5,) alakból látszik, hogy a bemenet egyetlen ötelemű tengely; az axis nélküli mean ezt az egész tengelyt redukálja skalárrá. A matematikai szumma tehát a NumPy redukció közvetlen rokona.

Önellenőrzés: el tudja-e mondani, mit jelent a ∑i=15ei2\sum_{i=1}^{5} e_i^2 alsó és felső indexe? Tudja-e, miért range(len(errors)) a Python-megfelelő? Meg tudja-e nevezni a 3.63.6 mértékegységét, és azt, hogy miért nem „3.6 nap”? Ha mind a háromra van egy-egy pontos mondata, a jelölésből végrehajtható számítás lépése működik.

A tanítóhalmazon számított MSE önmagában nem torzítatlan becslése az új adatokon várható modellhibának. Mindig jelöljük, hogy a hibavektor a tanító-, validációs vagy tesztmintán keletkezett.

9.1.10 Rövid összefoglaló

A matematikai jelölés nem dísz, hanem futtatható utasítás. A szumma alsó és felső indexe a ciklushatárt adja; a hívott függvény a képlet; a paraméter a rögzített szám. A tartomány és a mértékegység nélkül a szám félrevezető: az ötértékű hibavektor MSE-je 3.6nap23.6\ \mathrm{nap}^2, RMSE-je 1.8971.897 nap. A kézi, ciklusos és NumPy-útvonal azonos eredményt adott, amit 10−1210^{-12} toleranciás invariant ellenőriz. A szumma négyzetének és a négyzetek szummájának felcserélése a tipikus jelölési hiba. A következő alfejezet ezt a függvényfogalmat az exponenciális és logaritmikus skálákra alkalmazza.