Práce s číslyJiří ZnamenáčekPočítání s konečnou přesností2020-10-02
Tato přednáška je velmi – velmi! – jemným úvodem do problémů numerické matematiky a v podstatě jejím jediným účelem je vás přesvědčit, že dostat z počítače (numerický) výsledek, kterému můžete věřit, je – pro někoho možná překvapivě – dosti netriviální záležitost.
Stejně jako mnohde jinde bývá při numerických výpočtech rychlost vykoupena přesností a vše ještě komplikují nároky na zdroje. Často je sice teoreticky možné dostat výsledek s téměř libovolnou požadovanou přesností, ale jednak na to nemusíte mít zrovna k dispozici zdroje, a druhak byste se také výsledku nemuseli v dohledné době vůbec dočkat. Nastupujete tak kompromis, kterému je potřeba alespoň v hrubých rysech rozumět, abyste pak nezplakali nad výdělkem.
Přednáška inspirována materiálem Introduction to Computational Linear Algebra od Philippe B. Laval'a a Chyby a jejich šíření od Petra Habaly.
Úkolem numerické matematiky je – velmi zjednodušeně – získat pro vstupní matematický výraz číselný výsledek s požadovanou přesností.
Nebo také jinak řečeno výsledek, kterému je možno věřit.
Cesta k tomuto cíli ale obsahuje několikero těžkotonážních překážek z následujících dvou skupin:
chyby vstupu:
Abychom měli co počítat, musíme algoritmu poskytnout nějaká vstupní data. A už to se v naprosté většině případů nedá udělat přesně a bez chyb.
chyby výpočtu (algoritmu):
Ať se nám to líbí nebo ne, počítače nejsou ani náhodou nekonečně přesné (spíš jsou mnohem nepřesnější, než si asi většina lidí myslí) a i použité algoritmy jsou často kompromisem mezi přesností, rychlostí a dostupnými zdroji.
Mezi typické chyby na vstupu do výpočtu patří:
nepřesná vstupní data – máloco je možné změřit přesně a bez chyby;
Ještě tak třeba počet účastníků experimentu, pokud to nebude příliš veliké číslo.
chyby zadání dat – např. data z měření musíme nějak dostat do počítače a cestou se může stát všelicos;
Stačí si představit, že je třeba musí někdo ručně přepsat. A i pokud tam „přitečou“ automaticky po drátě, může stejně dojít k chybám při přenosu.
nemožnost přesně reprezentovat všechna čísla.
Asi naprostá většina vstupních hodnot jsou reálná čísla, a těch je prostě ℵ_1 a nedají se tudíž ani náhodou pro potřeby počítače všechna nějak přesně zadat. (Ostatně už s velkými čísly z množiny o mohutnosti ℵ_0 by byl problém :-)
Málokdy tudíž dostane algoritmus na vstupu přesná data, takže těžko doufat v bůhvíjakou přesnost výpočtu. (Spíš můžeme být rádi, když to nebudou rovnou úplné nesmysly.)
I kdybychom nakrásně měli na vstupu přesná (a správná) čísla, prakticky jakýkoliv výpočet v počítači stejně neposkytne přesný výsledek z následujících důvodů:
nemožnost přesné reprezentace všech čísel v počítači (round-off error) – z pochopitelných technických důvodů mají počítače na zaznamenání čísel pouze konečné místo;
Takže i čísla, která je možno teoreticky zapsat přesně, v počítači přesně nezaznamenáme.
šíření chyb při výpočtu – provést výpočet s nepřesnými čísly znamená vypropagovat tuto chybu dál a často ji ještě zvětšit;
Nehledě na to, že i s náhodou přesnými čísly se můžeme snadno dostat mimo technické meze počítače a obdržet v lepším případě jen nepříliš přesný výsledek, v horším pak úplný nesmysl.
chyba ořezu (truncation error) – prakticky není možné provést teoreticky přesný nekonečný výpočet a musíme ho tak někde ukončit, „zaříznout“.
Například přímo nespočitatelnou funkci sice můžeme nahradit jejím Taylorovým rozvojem (tedy vlastně sčítáním a násobením), ale prakticky se budeme muset omezit pouze na jeho několik prvních členů a všechny ostatní zahodit.
PS: Navzdory tomutu všemu kupodivu někdy může výpočet chybu na vstupu i zmenšovat, ale to je spíše výjimka.
Jako příklad na chybu ořezu si zkusme spočítat určitý integrál I = ∫_0^{1/2}{e^{x^2}}dx. Jelikož primitivní funkci k e^{x^2} neznáme, musíme si pomoci aproximací. Kupříkladu rozvojem podle Taylora:
Pokud uvedenou nekonečnou řadu zintegrujeme..
..a vyhodnotíme si prvních pár členů (integrál součtu je aditivní a dolní mez dává vždycky nulu)..
člen
výsledek
celkový součet
x
1/2
1/2 = 0.5
{x^3}/3
1/{3·2^3} = 1/24
1/2 + 1/24 = 13/24 ≐ 0.5416666666666666
{x^5}/{5·2!}
1/{5·2!·2^5} = 1/320
13/24 + 1/320 = 523/960 ≐ 0.5447916666666667
{x^7}/{7·3!}
1/{7·3!·2^7} = 1/5376
523/960 + 1/5376 = 4883/8960 ≐ 0.5449776785714285
..vidíme, že každý další člen rozvoje přispívá k výsledku čím dál tím méně (nehledě na to, že při konečné přesnosti se časem dostaneme k číslům nerozeznatelným od nuly), takže si můžeme dovolit spočítat jich pouze tolik, kolik potřebujeme.
V kapitole o chybách výpočtů uvidíme, že odečítat dvě sobě blízká čísla si říká o průšvih (tím větší, čím jsou sama tato čísla větší). Je však poučné vidět, že čísla nemusí být ani tak moc vysoká, abychom začali dosti významným způsobem ztrácet přesnost výsledků.
Výraz f(x)=x(√{x+1}-√x) sice obsahuje odečítání, ale pro vstup x=500 by nikdo asi žádný zásadní podraz nečekal. Zkusme si ho vypočítat při přesnosti 6 míst se zaokrouhlováním:
Uvedený výraz je přitom algebraicky ekvivalentní výrazu g(x)=x/{√{x+1}+√x}, který žádné odečítání neobsahuje a po dosazení (ve stejné přesnosti) dává kapku jiný výsledek:
Pokud necháme spočítat f(500) v dostupné přesnosti pro typ float, obdržíme 11.174755300746853, tudíž vidíme, že výraz bez odečítání poskytuje přesnější výsledek než algebraicky ekvivalentní výraz, který odečítání obsahuje.
Některé výpočetní metody jsou tzv. stabilní, tzn. že vstupní chyby se při nich nezvětšují (a někdy dokonce i zmenšují). Jenže mnohé jiné patří pro změnu mezi metody nestabilní, u nichž je chování dosti nepředvídatelné a pro některé vstupy doslova „vražedné“. Je-li výstup metody (myšleno její chyba) silně závislá na volbě vstupních dat, říkáme pak, že taková úloha je špatně podmíněná.
Jako příklad špatně podmíněné nestabilní metody si ukážeme na první pohled zdánlivě nevinný výpočet determinantu, v němž za proměnnou x budeme dosazovat neurčité číslo (101±0,5), tedy vstupní relativní chyba jest necelého půl procenta:
Pro střední hodnotu x=101 jest výsledek
|{\table 100, 100; 100, x}| = |x=101| = 100·101-100·100 = 100.
Stačí však dosadit x=100,5 a máme
100·100.5-100·100 = 50,
což je najednou relativní chyba výsledku obludných 50 %, což představuje stonásobek vstupní 0_o
Přitom stačí, aby se matice lišila v jednom jediném znaménku a hned je všechno jinak:
|{\table 100, -100; 100, x}| = |x=101| = 100·101+100·100 = 20100,
zatímco pro x=100,5 dostaneme
100·100.5+100·100 = 20050,
což je relativní chyba stěží čtvrtina procenta, takže dokonce méně než na vstupu!
Přitom pokud by původní matice obsahovala jiná čísla, bude chování mnohem rozumnější – pro střední hodnotu x=101 platí
|{\table 100, 130; 120, x}| = 100·101-130·120 = -5500
a pro krajní chybovou pak v tomto případě x=100,5 zase
100·100.5-130·120 = -5550,
což je relativní chyba pouze 1 % (tedy dvojnásobek vstupní).
Vidíme tedy, že výpočet determinantu – ač sám zcela triviální – velmi silně závisí na vstupních datech (kam zahrnujeme i dopředu známé prvky matice) a relativní chyba výsledku se pohybuje nečekaně od vítané stability po zcela explozivní neurčitost.
Díky konečnému počtu míst pro uložení reálného čísla v počítači existuje nejen největší zaznamenatelné číslo, ale především také nejmenší vyjádřitelné číslo. Prakticky pak můžeme například součinem dvou velkých čísel přetéct mimo zaznamenatelný rozsah (a podle implementace se dokonce třeba objevit na opačném konci číselné osy) nebo naopak snadno podtéct a dostávat nulu tam, kde není.
Jelikož čísla pro výpočet musíme převést na stejný exponent (jinak s nimi pracovat neumíme), můžeme při nevhodném zřetězení vstupních dat některá z nich rovnou ztratit (právě tím převodem – buď se zcela marginalizují nebo se rovnou dostanou mimo zaznamenatelný rozsah). Proto se například při sčítání mnoha prvků čísla nejdříve seřadí a teprve potom se směrem od nejmenšího sečtou.
Nemůžeme-li si řazení dovolit, můžeme sečíst N čísel (x[0] až x[N-1]) například pomocí Kahanova sčítacího algoritmu (viz např. compensated summation), který „ztracené“ části čísel přenáší mezi jednotlivými kroky:
c, s = 0, x[0]
for i in range(1, N):
x = x[i] - c
y = s + x
c = (y - s) - x # A díky chybám právě neplatí, že c == 0 .
s = y