A Northstar késési modelljében (lásd Kérdés, célváltozó és döntési helyzet) rendelésenként becsülünk egy kockázatot. A modell bemenete tartalmazhatja a kosárértéket, a távolságot vagy a prioritási jelzést, kimenete pedig egy valószínűség. Ha egy ilyen modellt ábrázolunk, azonnal felmerül a kérdés: egy adott pontban mennyire meredek a görbe? A derivált (derivative) erre a pillanatnyi változási rátára ad számot. A derivált előjele és nagysága mondja meg, hogy egy kis bemeneti növelés körülbelül mennyivel növeli vagy csökkenti a kimenetet, ezért később az optimalizáló algoritmusok és a veszteségfüggvények elemzésének is alapja.
Ebben az alfejezetben a bemenet egy valós szám , a modell egy függvény, a megfigyelési egység pedig egy konkrét kiértékelési pont . A cél az meredekség numerikus becslése és ellenőrzése. A tanuló labor két tesztfüggvényt használ: és a logisztikus függvényt. Az egység kulcsfontosságú: ha mértékegysége km és értéke eurócent, a derivált egysége eurócent/km.
Induló feladat a megoldás előtt: becsüld meg kézzel meredekségét az pontban a 3 és 4 között húzott szelővel! A szelő meredeksége . Vajon az érintő ennél nagyobb vagy kisebb? A függvény gyorsulva nő, ezért a 3 és 4 közötti átlagos meredekség nagyobb, mint a 3-beli pillanatnyi érték; előzetesen nagyjából 6 körüli eredményt várunk.
Az függvény grafikonján az és pontokon átmenő egyenes a szelő. A szelő meredekségét differenciahányadosnak (difference quotient) nevezzük:
ahol a lépésköz (step size). A képletben és ugyanazon mértékegységben értendő, előjeles; negatív hátrafelé vett differenciát ad. A hányados egysége , vagyis a kimenet egysége osztva a bemenet egységével. Ha eurócentben, kilométerben adott, a differenciahányados eurócent/km dimenziójú.
Kis számpélda , mellett:
A sorozat a 6 felé tart. Az ábrán a szelő egyre jobban rásimul az érintőre, ahogy a két metszéspont közeledik egymáshoz. A következő kód a szelőt és az érintőt rajzolja fel:
import numpy as np
import matplotlib.pyplot as plt
def f_sq(x):
return x * x
a = 3.0
h = 1.0
tangent_slope = 2.0 * a # f'(x)=2x, tehát f'(3)=6
xs = np.linspace(2.0, 4.5, 200)
plt.figure(figsize=(6.5, 4))
plt.plot(xs, f_sq(xs), label="$x^2$", color="black")
plt.plot([a, a + h], [f_sq(a), f_sq(a + h)], marker="o",
label="szelő (h=1)")
plt.plot([a - 0.6, a + 0.6],
[f_sq(a) - 0.6 * tangent_slope, f_sq(a) + 0.6 * tangent_slope],
linestyle="--", label="érintő")
plt.xlabel("x")
plt.ylabel("f(x)")
plt.title("Szelő és érintő az x=3 pontban")
plt.legend()
plt.tight_layout()
plt.savefig("derivative_segment.png", dpi=120)Az ábrán a szelő meredeksége 7, az érintőé — a következő szakasz analitikus képlete szerint — 6.
A Határérték mint közelítési folyamat alfejezetben a határértéket úgy vezettük be, mint közelítési folyamatot: egy sorozat értékei egy számhoz tartanak anélkül, hogy feltétlenül elérnék. A differenciahányados-sorozat ugyanezt a gondolatot használja. Ha a
határérték létezik és véges, akkor azt az függvény pontbeli deriváltjának nevezzük, és -val jelöljük. A jelölés nem jelenti azt, hogy -t behelyettesíthetünk; a alak önmagában értelmetlen. Azt jelenti, hogy a hányadost tetszőlegesen kicsi, de nullától különböző lépésközökkel közelítjük.
A derivált geometriai jelentése az érintő meredeksége, adatmodellezési jelentése pedig pillanatnyi változási ráta. Kis -ra a függvényérték lineárisan közelíthető:
Ez a linearizálás a későbbi optimalizáló lépések alapja: ha pozitív, a függvény lokálisan nő, ha negatív, akkor csökken. A meredekség előjele mondja meg, merre érdemes elmozdulni.
Fontos: pontbeli mennyiség, általában függ értékétől. Az deriváltfüggvénye , tehát , de . A konstans függvény deriváltja mindenütt 0. Erre a különbségre a gyakori tévedéseknél visszatérünk.
A fenti egyoldali, előre vett forward differencia mellett gyakori a centrális differencia (central difference):
Ez a két egyoldali hányados átlaga, és szimmetrikus az pont körül. Az példában -ra:
Itt a centrális differencia minden nullától különböző -ra pontosan 6-ot ad, mert a másodfokú hibatag kiesik. Általános függvénynél a centrális differencia sem pontos, de a csonkítási hibája jellemzően -tel arányos, míg a forward differenciáé -val arányos. Csökkenő mellett tehát a centrális változat gyorsabban javul.
A numerikus becslésnek két hibaforrása van. (1) A csonkítási hiba (truncation error) abból adódik, hogy a szelő nem azonos az érintővel; ez nagy -nál dominál. (2) A kerekítési hiba (rounding error) abból adódik, hogy és lebegőpontos közelítés; nagyon kis esetén a különbségképzés elveszti az értékes jegyeket, és a hányados zajossá válik.
A float64 gépi pontosság . A forward differenciánál a hasznos lépésköz nagyságrendje , a centrálisnál . A pontos optimum függ a függvénytől és a skálázástól, ezért a lépésközt mindig a konkrét feladaton kell ellenőrizni. A szabály itt sem az, hogy „minél kisebb , annál jobb”.
A következő önálló szkript a két tesztfüggvényen becsüli a
deriváltat, kiírja a hibát több lépésközre, és log-log ábrát ment. Nincs
véletlenszám, ezért seedre nincs szükség; a futási eredmény a NumPy
float64 kerekítésétől függhet, ezért platformok közötti bitazonosságot
nem ígérünk. A kódot a lab projekt gyökérkönyvtárában, a
projekt Python-környezetében futtatjuk:
# platform: Linux/macOS bash, munkakönyvtár: a könyv lab/ projektgyökere
python s041_derivative_demo.pyimport numpy as np
import matplotlib.pyplot as plt
def f_sq(x):
"""f(x)=x^2, skalár bemenetre."""
return x * x
def df_sq_exact(x):
"""f'(x)=2x analitikus derivált."""
return 2.0 * x
def sigmoid(x):
"""Numerikusan stabil logisztikus függvény.
A közvetlen 1/(1+exp(-x)) nagy negatív x-re túlcsordulást
kockáztat; ez az alak mindkét előjel mellett stabil.
"""
return np.exp(np.minimum(x, 0.0)) / (1.0 + np.exp(-np.abs(x)))
def d_sigmoid_exact(x):
"""sigma'(x)=sigma(x)(1-sigma(x))."""
s = sigmoid(x)
return s * (1.0 - s)
def forward_difference(f, x, h):
return (f(x + h) - f(x)) / h
def central_difference(f, x, h):
return (f(x + h) - f(x - h)) / (2.0 * h)
# --- 1. rész: x^2 az x=3 pontban -------------------------------
a = 3.0
exact_sq = df_sq_exact(a)
print(f"x^2, a={a}: analitikus derivált {exact_sq:.6f}")
for h in (1e-3, 1e-5, 1e-8):
fd = forward_difference(f_sq, a, h)
cd = central_difference(f_sq, a, h)
print(f"h={h:.0e}: forward {fd:.9f}, central {cd:.9f}")
# --- 2. rész: logisztikus függvény az x=1 pontban --------------
b = 1.0
exact_sig = d_sigmoid_exact(b)
print(f"\nsigma, x={b}: analitikus derivált {exact_sig:.9f}")
h_sig = 1e-6
fd_sig = forward_difference(sigmoid, b, h_sig)
cd_sig = central_difference(sigmoid, b, h_sig)
print(f"h={h_sig:.0e}: forward {fd_sig:.9f}, central {cd_sig:.9f}")
# --- 3. rész: hiba a lépésköz függvényében ----------------------
hs = np.logspace(-16.0, -1.0, 61)
err_f = [abs(forward_difference(f_sq, a, h) - exact_sq) for h in hs]
err_c = [abs(central_difference(f_sq, a, h) - exact_sq) for h in hs]
plt.figure(figsize=(7, 4.2))
plt.loglog(hs, err_f, marker=".", label="forward")
plt.loglog(hs, err_c, marker=".", label="central")
plt.xlabel("h lépésköz")
plt.ylabel("abszolút hiba")
plt.title("x^2 numerikus deriváltjának hibája x=3-ban")
plt.legend()
plt.tight_layout()
plt.savefig("s041_derivative_error.png", dpi=120)
print("\nÁbra elmentve: s041_derivative_error.png")A szemléltető, elméletileg várt kimenet a képletekből számolva (a helyi futás utolsó jegyekben a hardverfüggő kerekítések miatt eltérhet):
x^2, a=3: analitikus derivált 6.000000
h=1e-03: forward 6.001000000, central 6.000000000
h=1e-05: forward 6.000010000, central 6.000000000
h=1e-08: forward 6.000000010, central 6.000000000
sigma, x=1: analitikus derivált 0.196611933
h=1e-06: forward 0.196611888, central 0.196611933
Ábra elmentve: s041_derivative_error.png
A forward differencia
melletti értéke a csonkítási hiba csökkenése és a kerekítési zaj
megjelenése közötti átmenetet mutatja. A scipy.optimize-ot
itt még nem hívjuk; a következő alfejezetek gradiens alapú keresői erre
a meredekségfogalomra épülnek.
A forward differencia esetén:
Az analitikus érték 6, tehát az abszolút hiba pontosan . Ez a várt -val arányos csonkítási hiba: másodfokú függvénynél az eltérés éppen a lépésköz nagyságrendjébe esik. A centrális differencia ugyanerre a lépésközre pontosan 6-ot ad, mert a másodfokú hibatag kiesik. Ez egyben a kód invariánsa: -re a centrális differencia matematikailag 6, numerikusan pedig 6-hoz közeli mindaddig, amíg a kerekítési hiba kicsi.
A logisztikus függvény deriváltjának analitikus alakja az összetett függvény szabályából adódik, amelyet a Láncszabály és számítási gráf tárgyal részletesen; itt csak felhasználjuk:
Az pontban , ezért
A centrális differencia mellett az elméleti hibája nagyságrendű, azaz alatti; a fenti várt kimenetben 9 tizedesjegyre egyezik az analitikus értékkel.
A hibaábra alakja a következő: nagy -nál a forward görbe -val, a centrális -tel arányosan csökken; körülbelül (forward), illetve (centrális) alatt a kerekítési zaj felnövekszik, és a hiba újra nő. Az ábra egyszerre mutatja a csonkítási és a kerekítési hibát, ezért alkalmas arra, hogy a lépésköz-választást indokoljuk.
A numerikus differencia hányadosának hibája a lépésköz csökkentésével nem monoton javul: a csonkolási hiba mérséklődik, miközben a kerekítési hiba és a kivonási pontatlanság felerősödhet. A hibatáblázatot ezért logaritmikus -rácson is érdemes ábrázolni.
Értelmezés és kézi kontroll. Egy fiktív szállítási költségmodell eurócent, ahol a távolság km-ben. Add meg a derivált egységét! Számold ki kézzel km-nél a forward differenciahányadost km-rel, és hasonlítsd össze az analitikus eurócent/km értékkel!
Implementálás. Írj
central_difference(f, x, h) függvényt, amelynek bemenete
egy skalár függvény, egy pont és egy lépésköz, kimenete pedig egy
lebegőpontos szám. Alkalmazd a logisztikus függvényre az
pontban,
lépésközzel! Add meg az abszolút hibát a
analitikus alakhoz képest!
Hibadiagnózis. Egy kolléga ezt a kódot futtatta:
def bad_derivative(f, x):
h = 1e-16
return (f(x + h) - f(x)) / h
print(bad_derivative(lambda x: x * x, 3.0))Azt kapta, hogy a függvény 0.0-t ír ki, pedig a helyes
érték 6 lenne. Mi a numerikus hiba ezzel a lépésközzel, és milyen
második veszély fenyegetne akkor is, ha a lépésköz valamivel nagyobb
lenne? Javítsd úgy, hogy a forward differenciánál szokásos
nagyságrendű lépésközt használjon!
1. A derivált egysége eurócent/km, mert a számláló eurócentben, a nevező km-ben van. Kézi számolás:
Az analitikus derivált , tehát eurócent/km. A forward becslés hibája 3 eurócent/km, relatíve ; ez a nagy lépésköz miatt várható. A centrális differencia ugyanezzel a -val pontosan 120-at adna, mert a másodfokú függvény hibatagja kiesik.
2. Egy lehetséges implementáció:
import math
def central_difference(f, x, h):
return (f(x + h) - f(x - h)) / (2.0 * h)
def sigmoid_stable(x):
if x >= 0.0:
return 1.0 / (1.0 + math.exp(-x))
e = math.exp(x)
return e / (1.0 + e)
x = 1.2
h = 1e-5
est = central_difference(sigmoid_stable, x, h)
exact = sigmoid_stable(x) * (1.0 - sigmoid_stable(x))
print(f"becsült {est:.12f}, analitikus {exact:.12f}, "
f"abszolút eltérés {abs(est - exact):.2e}")Az pontban , a derivált analitikus értéke . A centrális differencia mellett a -tel arányos csonkítás miatt nagyságrendű abszolút hibát ad; a fenti kimenet szemléltető, a tényleges futást a saját környezetben kell elvégezni. Ellenőrzési tolerancia: az eltérés alatt marad.
3. Az első hiba: kisebb, mint a float64 fél ulp-je az pontban (az ulp ott ), ezért az összeg ugyanarra a lebegőpontos számra kerekedik, mint a 3. A számláló így pontosan 0, a hányados 0. A második veszély: ha a lépésköz valamivel nagyobb lenne, de még mindig nagyon kicsi, két közeli szám különbségképzése katasztrofális kiejtést (catastrophic cancellation) okozna, és a hányados zajossá válna. Javítás:
def good_forward(f, x, h=1e-8):
return (f(x + h) - f(x)) / h
print(good_forward(lambda x: x * x, 3.0)) # várt: 6.00000001 körülA várt érték az analitikus 6-tól nagyságrendű hibával, azaz -nal tér el; a tesztben a 6-tól mért abszolút eltérés alatt marad.
4. A láncszabály (amelyet a Láncszabály és számítási gráf alfejezetben igazolunk) szerint, ha a belső függvény , akkor
Az pontban és , tehát
A gyakorlati jelentés: a modell szerint ebben a pontban 100 euró kosárérték-növekedés körülbelül 0,3-mal növeli a becsült késési valószínűséget. Lineáris közelítéssel egy egységnyi növelés -ről -ra emeli a becslést. A centrális differenciás ellenőrzés mellett körüli értéket ad; az elfogadható eltérés alatt van.
A leggyakoribb tévedés, hogy a deriváltat egyetlen számmal azonosítjuk, mintha a függvény meredeksége állandó lenne. Ellenpélda: az függvény deriváltja -ban 6, de -ben 20; ugyanaz a függvény más pontban más meredekséget ad. Ha a szállítási költség deriváltját kiszámoljuk 2 km-nél, az nem érvényes 50 km-nél. A konstans függvény a kivétel: például deriváltja mindenütt 0. Hasonló tévedés, hogy a legkisebb mindig pontosabb; a hibaábra éppen azt mutatja, hogy túl kis lépésköznél a kerekítési zaj elrontja a becslést.
Visszautalás: a Vektor, skalárszorzat, norma és távolság alfejezetben a vektort rendezett számlistaként, a különbségvektort pedig a távolság alapjaként vezettük be. A differenciahányados egy dimenzióban ugyanezt a különbséget skálázza: a függvény kimenetének változását osztjuk a bemenet változásával. A Reláció, NULL és elemzési szemcsézettség megfigyelési egység fogalma itt a kiértékelési pontban tér vissza: a derivált mindig egy konkrét ponthoz tartozik, és nem vihető át automatikusan más pontra.
Önellenőrzés: (a) Miért nem helyettesíthetjük be -t a differenciahányadosba? (b) Mi a különbség a forward és a centrális differencia képlete között? (c) Milyen mértékegysége van a deriváltnak egy euró-költség és km-távolság függvénynél? (d) Miért nő meg a numerikus hiba nagyon kis esetén?
A differenciahányados a szelő meredeksége, és ahogy a lépésköz nullához tart, az érintő meredekségét, a deriváltat közelíti. A derivált pontbeli változási ráta, mértékegysége a kimenet és a bemenet egységének hányadosa. Numerikusan a forward differencia egyszerű, a centrális differencia szimmetrikus és általában pontosabb, de a lépésközt a csonkítási és a kerekítési hiba egyensúlya szabja meg. Az és a logisztikus függvény laborja kézi kontrollal és hibaábrával igazolja ezt. A következő alkalmazás a Parciális derivált és gradiens parciális derivált, amely több bemeneti koordinátára terjeszti ki ezt az egyváltozós meredekséget.