Underfitting: Pár szó a modellkomplexitásról

Meszaros Zoltan Gabor
9 min readApr 18, 2024

--

Amikor készítünk egy fényképet a nyári vakációnkról, a valóság egy számunkra fontos szeletét akarjuk megőrizni, adott esetben az a célunk, hogy mások, további magyarázat nélkül is értelmezni tudják a látottakat.
Valójában a fénykép elkészültekor egy modellt építettünk.

kapcsolat: (24) Zoltán Mészáros | LinkedIn

forrás: Fortepan / Doffek Ágnes

Az 1900-as évek elején az Adriai tenger partján készült alkotás tökéletes példa.
A valóságból kiragadt elemek és azok fontossága egyértelmű.

  1. Előtérben állnak a személyek, akik részt vettek a családi eseményen. Felismerhetőek, dominálnak a fényképen.
  2. Háttérben kevésbé előkelő pozícióban talán a család barátai, távolabbi hozzátartozók, személyzet.
  3. Látjuk a hullámzó tengervizet ami keretezi a fényképet kijelölve a helyszínt, ugyanakkor már homályban hagyva a pontos lokációt. Valószínűleg a fényképész számára értékesebb volt a közösség, a család együttlétének megörökítésé ebben az adott pillanatban.
  4. Hiányoznak a színek. Ha a technológia lehetőséget adott volna ennek a rétegnek a rögzítésére, valószínűleg akkor is elhanyagolható lenne a kép mondanivalója szempontjából.
  5. Feltűnőek a ruhák, a századelő középosztálybeli viselete, mely bár alapvető adottsága volt a kornak és így a képnek is, de közvetett módon segít nekünk a korszak megjelölésében.

Mindezt 120 év távlatából ösztönösen meg tudjuk állapítani.

Pontosan ilyen egy jól hangolt matematikai modell. Ha vesszük idősorunkat, és meg akarjuk ragadni annak a valóságnak szerkezetét amit ábrázolni próbálunk, olyan érzékenyre kell állítani az azt leíró egyenletet, ami képes megragadni a valóságot. Össze kell válogatnunk a leglényegesebb elemeket.
Mint minden az adattudományban ez nyers adatok függvénye. Egy komplex adatsort mely magas variánciával, szezonális trendekkel, kisze-kusza ciklusokkal rendelkezik csak egy ennek megfelelő modell képes visszaadni. Pont mint a példának vett fotónk szereplői, a helyszín, a történeti keret egysége.

Nézzünk az előbbi képre ha “általánosítunk” kicsit rajta, és elmosódnak a részletek.

forrás: Fortepan / Doffek Ágnes elhomályosított verzió

Ez a példa valószínűleg nem állta volna ki az idő próbáját. Egyszerűen nem használható. A kép lényegi információtartama elvész, csak tippelni tudnánk arra mi szerepel a képen, mikor készült, különösen ha az eredetinek nem lennénk tudatában. 1900-as évek Adriája? Vagy a nyolcvanas évek Balatonfürede? Elvesztettük a kontextust.

Gondoljunk erre a második képre úgy, mint modellillesztésnél az egyik fő adatelemzői hibára amit elkövethetünk.
Ez pedig az alulillesztés (underfitting) problémaköre, mely akkor jelentkezik ha a valóságot leíró adatokra fittelt modell nem elég komplex.

A továbbiakban a metaforát kibontjuk és megnézzük annak matematikai hátterét.

01. Elemzési csomagok betöltése

Töltsük be az elemzés támogató python könyvtárakat. A korábbi bejegyzésekhez képest újdonság az sklearn csomag LinearRegression funkciójának importálása. Az sklearn mint ML programcsomag talán a legszélesebb körben használt eszköz. Még a legbonyolultabb elemzések is sokszor egy egyszerűbb lineáris modellből indulnak ki, mint összehasonlítási alap. Tegyünk mi is így. További újdonság, hogy az ExponentialSmoothing megvalósításához szükséges eszközöket ezúttal a statsmodel könyvtárból vesszük (amiből a Darts csomag is építkezik).

# adatbetöltés, fomrázás, ábrázolás
import pandas as pd
import math
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

# használt hibametrika
from sklearn.metrics import mean_absolute_error

# Exp. Smoothing toolkit
from statsmodels.tsa.holtwinters import SimpleExpSmoothing

# Lineáris regressziós modell
from sklearn.linear_model import LinearRegression

# hibaüzenetek kikapcs
import warnings
warnings.filterwarnings("ignore")

02. “Baseline” modell: Egyszerű átlagolása az idősornak

Készítsünk egy függvényt ami egy Excel fájlból betölti az értékesítési adatainkat, melyből keresleti predikciót szeretnék készíteni. Számoljuk ki a teljes idősor átlagát, adjunk meg egy hibametrikát (MAE — mean absolut error) mellyel értékelni tudjuk az illesztés minőségét, és lehetőséget ad az összehasonlításra. Az eredményeket ábrázoljuk egy grafikonon.

def run_analysis():
# Adatok betöltése
df = pd.read_excel("raw_data_store_01.xlsx")
df["nr"] = range(1, len(df) + 1)
df = df[["date", "nr", "demand"]]

# Kereslet átlagának kiszámítása
average_demand = df["demand"].mean()
df["fitted_demand"] = average_demand

# MAE kiszámolása
mae = mean_absolute_error(df["demand"], df["fitted_demand"])

# Grafikon megjelenítése
plt.figure(figsize=(10, 5))
plt.plot(df["nr"], df["demand"], label="Actual Demand", color="grey")
plt.axhline(y=average_demand, color='blue', linestyle='--', label="Average Demand")
plt.title(f"Demand Forecast Using Simple Average - MAE: {mae:.2f}")
plt.xlabel("nr")
plt.ylabel("Demand")
plt.legend()
plt.grid(True)
plt.show()

run_analysis()

Az ábra X tengelye idősorunk, szürke vonallal látható a tényadataink, szaggatott kék vonallal pedig az átlag, mint alap predikciós stratégia. Ebben az estben egy vonalat illesztettünk az adathalmazra, predikciónk is ennek megfelelőlen változatlan maradna a következő periódusra is.

Átlag illesztese

Egy átlag természetesen nem fog túl jó illeszkedést biztosítani, haladjunk egy kicsit tovább, és döntsük meg ezt a vonalat.

03. Lineáris trend illesztése

Lineáris trend alkalmazása esetén már képesek vagyunk visszaadni egy hosszú, akár több éven keresztül húzódó fő tulajdonságot. Ezt a vásárló igénybe jelentkező csökkenést sikerül már ezzel a komplexitási szinten is kifejezni.
Ahogy a MAE értéken is látszik 32 000-ről 31 000 egységre javultunk.

def run_analysis():
# Adatok betöltése
df = pd.read_excel("raw_data_store_01.xlsx")
df["nr"] = range(1, len(df) + 1)
df = df[["date", "nr", "demand"]]

# Lineáris regresszió
X = df[["nr"]]
y = df["demand"]
model = LinearRegression()
model.fit(X, y)
df["fitted_demand"] = model.predict(X)

# MAE kiszámolása
mae = mean_absolute_error(y, df["fitted_demand"])

# Grafikon megjelenítése
plt.figure(figsize=(10, 5))
plt.plot(df["nr"], df["demand"], label="Actual Demand", color="grey")
plt.plot(df["nr"], df["fitted_demand"], label="Fitted Demand", color = "blue", linestyle='--')
plt.title(f"Demand Forecast - MAE: {mae:.2f}")
plt.xlabel("nr")
plt.ylabel("Demand")
plt.legend()
plt.grid(False)
plt.show()

run_analysis()
lineáris trend illesztése

Haladjunk tovább az összetettségben, használjuk az Exponential Smoothing technikát.

04. Exponential Smoothing

A három modellünk közül a legkomplexebb modellben 0.19 alpha értékekkel közelítjük az adatsor vonulását. (múltbeli adatokra való emlékezés mértéke)

def run_analysis_2(alpha = 0.19):
# Adatok betöltése
df = pd.read_excel("raw_data_store_01.xlsx")
df["nr"] = range(1, len(df) + 1)
df = df[["date", "nr", "demand"]]

# Egyszerű exponenciális simítás
model = SimpleExpSmoothing(df["demand"]).fit(smoothing_level=alpha, optimized=False)
df["fitted_demand"] = model.fittedvalues

# MAE kiszámolása
mae = mean_absolute_error(df["demand"], df["fitted_demand"])

# Grafikon megjelenítése
plt.figure(figsize=(10, 5))
plt.plot(df["nr"], df["demand"], label="Actual Demand", color="grey")
plt.plot(df["nr"], df["fitted_demand"], label="Fitted Demand", color = "blue", linestyle='--')
plt.title(f"Demand Forecast - MAE: {mae:.2f}")
plt.xlabel("nr")
plt.ylabel("Demand")
plt.legend()
plt.grid(False)
plt.show()

# A függvény meghívása, ami végrehajtja az elemzést és megjeleníti az eredményeket
run_analysis_2()

A legkomplexebb modellünk szépen simítja az adatsort, adaptálódik a lokális átlagértékekhez és az enyhe, ciklikusnak tűnő mozgásokat is követi. Úgy tűnik a tényvilág adatkomplexitásához ez a típus áll a legközelebb jelenlegi eszköztárunkból.

Összehasonlítva a MAE értékeket a következőket látjuk:

“Baseline” átlag: 32 105
Linear trend: 30 312
Exp. Smoothing: 28 824

Ebből máris kitűnik, hogy az adataink természetéhez jobban alkalmazkodó ES modell 5%-kal ad jobb eredményt mint a lineáris verzió, illetve több mint 10%-kal teljesíti felül az átlagra szavazó változatot. Üzleti oldalról vizsgálva: ha a túl- illetve alulkészletünkből fakadó költségeket azonosnak vesszük, el tudjuk képzelni az éves budgetben a forkasztálás minőségének javításával milyen ugrásszerű költségcsökkenést érhetünk el!

A modell komplexitásának vizsgálata elvezet egy, az ML világába is fontos alapvető kérdéshez, és technikához:

Hogyan tudom meghatározni modellem mennyire stabil? Hogyan fog teljesíteni az eddig nem látott új adatokon? Hogyan tudok “kilépni” a modell világból? A válasz a tanuló és teszt adathalmazra bontás.

05. Tanulás és tesztelés

Gondoljunk egy iskolai szituációra. Hogy tudom értékelni egy tanuló teljesítményét? Dolgozatot íratunk, jellemzően olyan példákkal, amivel nem találkozott a diák megelőzőleg.
Ismerjük a megoldás menetét, (a modell megfelelő komplexitású) tehát képesek vagyunk egy általános igazságot felismerni. Valószínűleg, pontosan azzal a feladattal még nem találkoztunk, ami a dolgozatban szerepel, mégis jó megoldást tudunk adni.
Megelőlegezve egy későbbi témát: túltanulás esetén a helyzet fordított. Ebben az esetben úgy tanítottak minket, hogy képtelenek lennénk egy új, ismeretlen feladatot megoldani akkor, ha csak egy-két szám is más lenne a tesztben, mint a tanulópéldákban.

Biztosítanunk kell, hogy az adatsorunk két elkülönülő részre legyen bontva. A teszt halmaz lehetőséget ad a modell felépítésére és optimalizálására, a tréning szetten pedig ellenőrizni tudjuk annak valódi pontosságát új adatokon. Emlékezzünk: a modell ezt a teszt szettet soha nem “látta” korábban.

Mivel szembesülhetünk eredményül?

  • Egy alulfittelt modell adhat jó eredményt a tanuló halmazon, de gyengét a teszthalmazon.
  • Ha a modell a tréning halmazon nem szerepel jól, valószínűleg a teszt halmazon sem fog.

Készítsük el a tréning/teszt módszer szerint a kódolást:

  1. Töltsük be az adatsort az eddigiek szerint.
  2. Vágjuk a betöltött adatsort teszt és tréning részre (a példában 50%-nál vágok, a bemutatás érdekében, idősorok esetén jellemzően az utolsó periódust vágjuk le és hagyjuk teszt adathalmazban)
  3. Tanítsuk be a modellt a tanító halmazon.
  4. Végezzünk el a predikciót a teszt halmazon, úgy hogy összeillesszük a teszt és tanuló halmaz adatsorát
  5. Számoljuk ki a mindkét halmazra a hiba mértékét, majd ábrázoljuk mindezt együtt egy grafikonon.

Fontos: A modell fittelésénél figyeljünk arra, hogy a modell ne kapjon véletlenül sem információt a “jövőből”. Például ha paraméter optimalizálást bármilyen módon is, de végez a modell a tesztadatokon hibás eredményt fogunk kapni. Ez a Data Lakage jelensége.

def run_analysis_2(alpha = 0.19):
# Adatok betöltése a load_data függvény segítségével
df = pd.read_excel("raw_data_store_01.xlsx")
df["nr"] = range(1, len(df) + 1)
df = df[["date", "nr", "demand"]]


# Kiválasztjuk az első felét az adatoknak a tanításhoz
half_point = len(df) // 2
train_df = df.iloc[:half_point]
test_df = df.iloc[half_point:]

# Egyszerű exponenciális simítás a tanító adatokon
model = SimpleExpSmoothing(train_df["demand"]).fit(smoothing_level=alpha, optimized=False)

# Predikció az egész adathalmazon
df["fitted_demand"] = model.predict(start=0, end=len(df) - 1)

# MAE kiszámolása a tanító adathalmazon
train_mae = mean_absolute_error(train_df["demand"], df.loc[:half_point - 1, "fitted_demand"])

# MAE kiszámolása a teszt adathalmazon
test_mae = mean_absolute_error(test_df["demand"], df.loc[half_point:, "fitted_demand"])

# Grafikon megjelenítése
plt.figure(figsize=(10, 5))
plt.plot(df["nr"], df["demand"], label="Actual Demand", color="grey")
plt.plot(df["nr"], df["fitted_demand"], label="Fitted Demand", color="blue", linestyle='--')
plt.axvline(x=half_point, color='red', label='Training/Test Split', linestyle='--') # Piros vonal a felosztási pontnál
plt.title(f"Demand Forecast - Training MAE: {train_mae:.2f}, Test MAE: {test_mae:.2f}")
plt.xlabel("nr")
plt.ylabel("Demand")
plt.legend()
plt.grid(True)
plt.show()

# függvény indítása
run_analysis_2()
tanuló és teszt halmaz — ES

Képzeletben álljunk a múltat és jövőt elválasztó jelenpontra (piros szaggatott vonal) és tekintsünk rá a két halmazunkra. Az adatsor eleje a tanuló halmaz a múlt, még a piros vonal másik oldala a jövő, amit még a modell nem ismer.
A grafikon piros vonalán végeztünk tehát egy metszést az adathalmazon és keletkezett 50% tanulóadatunk. Ezen a halmazon futtatjuk a ES modellt, mely látszik a kék szaggatott vonal mozgásából, így próbálja lekövetni a változásokat 0.19 alpha értéken. A múltbeli adatok enyhe felülsúlyozása kitűnik: az időszak kezdetén egy lokális erősebb trend figyelhető meg, ami miatt a mi kék prediktív szaggatott vonalunk is magasabbra kerül.
A piros vonaltól jobbra találhatóak teszt adataink. Mivel a továbbra sem számolunk trenddel és szezonalitással (csak szintekkel, lásd korábbi bejegyzésem), a predikciónk egy konstans lesz. A hibametrikákat értelmezve a két időszak MAE értéke nagyon közeli.

Alkalmazzuk a gondolatmenetet a lineáris trendre is.

def run_analysis_2(alpha = 0.19):
# Adatok betöltése a load_data függvény segítségével
df = pd.read_excel("raw_data_store_01.xlsx")
df["nr"] = range(1, len(df) + 1)
df = df[["date", "nr", "demand"]]


# Kiválasztjuk az első felét az adatoknak a tanításhoz
half_point = len(df) // 2
train_df = df.iloc[:half_point]
test_df = df.iloc[half_point:]

# Egyszerű exponenciális simítás a tanító adatokon
model = SimpleExpSmoothing(train_df["demand"]).fit(smoothing_level=alpha, optimized=False)

# Predikció az egész adathalmazon
df["fitted_demand"] = model.predict(start=0, end=len(df) - 1)

# MAE kiszámolása a tanító adathalmazon
train_mae = mean_absolute_error(train_df["demand"], df.loc[:half_point - 1, "fitted_demand"])

# MAE kiszámolása a teszt adathalmazon
test_mae = mean_absolute_error(test_df["demand"], df.loc[half_point:, "fitted_demand"])

# Grafikon megjelenítése
plt.figure(figsize=(10, 5))
plt.plot(df["nr"], df["demand"], label="Actual Demand", color="grey")
plt.plot(df["nr"], df["fitted_demand"], label="Fitted Demand", color="blue", linestyle='--')
plt.axvline(x=half_point, color='red', label='Training/Test Split', linestyle='--') # Piros vonal a felosztási pontnál
plt.title(f"Demand Forecast - Training MAE: {train_mae:.2f}, Test MAE: {test_mae:.2f}")
plt.xlabel("nr")
plt.ylabel("Demand")
plt.legend()
plt.grid(True)
plt.show()

# Függvény indítása
run_analysis_2()
tanuló és teszt halmaz— LinTrend

Ahogy látszik a lineáris illesztés az új, nem látott adatokon nagyon rosszul teljesít. Továbbvisszük a lineáris trendet, ami a tesztperiódusban már értelmét veszti. Ez látszik is a két adathalmaz MAE értékének arányán: majdnem 50%-kal rosszabb tesztértéket kapunk.
Emlékezzünk: Amikor a teljes adathalmazra végeztünk illesztést, 30 000-es MAE értéket kaptunk, ami az SE eredményét közelítő eredmény. Mégis, amint elvégeztük az elemzést a teszt halmazon, látszik hogy a használatot erős fenntartással kellene kezelnünk.

06. Underfitting javítása

A modell érzékenységének javítására két módszerünk van. Egyrészt, ahogy a bejegyzésből is látszik választhatunk egy komplexebb működésű modellt. Másik lehetőség, hogy keresünk további változókat, melyek magyarázatot adnak bizonyos időszakok eltérő viselkedésére. Ilyen lehet például a hétvégi értékesítési adatok különbözősége, promóció. A nehézség abban áll, hogy statisztikai módszertanokat használva ez külön modell megépítését jelentené, hogy két vagy több modell együtt már érzékeny legyen a változásokra.
Ez a gyakorlatban nehezen kezelhető, viszont itt lép képbe a gépi tanulás eszközei, melyekkel már nem csak felismerni lesz képes a modellünk egy mintázatot, de képes lesz értelmezni is a történéseket. Ebben az irányban teszünk lépéseket a következőkben.

Kapcsolat

🚀Ha értékesnek találod a leírtakat, és szükségét látod egy hasonló elemzés elkészítésének, vagy adatelemzői munkakörbe keresel hosszútávon szakembert, kérlek keress meg alábbi Linkedin profilon:
(24) Zoltán Mészáros | LinkedIn

--

--