RAPORT Z BADAŃ WŁASNYCH OPRACOWANIE BIBLIOTEKI

advertisement
RAPORT Z BADAŃ WŁASNYCH
OPRACOWANIE BIBLIOTEKI DOKŁADNYCH OBLICZEŃ
NUMERYCZNYCH W JĘZYKU PYTHON
MARCIN CIURA
Streszczenie. W ramach badań opracowano bibliotekę procedur w języku
Python, która realizuje podstawowe operacje arytmetyczne i funkcje matematyczne. Dzięki zastosowaniu leniwie wartościowanych ułamków łańcuchowych
wyniki działań można podawać z dowolną dokładnością. Autor zaimplementował znane z literatury algorytmy wykonywania czterech działań arytmetycznych na ułamkach łańcuchowych oraz opracował nowe algorytmy obliczania
wartości funkcji wykładniczej, logarytmów oraz funkcji trygonometrycznych
i cyklometrycznych ułamków łańcuchowych.
1. Wprowadzenie
W języku programowania Python dostępny jest standardowy zmiennoprzecinkowy typ danych float i biblioteka funkcji matematycznych math. Ponadto od wersji 2.4 języka wprowadzono doń bibliotekę stałoprzecinkową decimal operującą na
liczbach dziesiętnych [1]. Do profesjonalnych obliczeń numerycznych można używać
dodatkowej biblioteki gmpy, stanowiącej interfejs do biblioteki obliczeń wielokrotnej
precyzji GNU Multiple Precision (GMP) [2].
Wspólną cechą tych bibliotek jest to, że wykonywane za ich pomocą obliczenia,
choć szybkie, są obarczone błędami zaokrągleń. Cała dziedzina nauki – analiza numeryczna – powstała, by szacować wielkość tych błędów, nawarstwiających się przy
stosowaniu algorytmów numerycznych. Idea niniejszej pracy jest odmienna: biblioteka będąca jej przedmiotem służy dokładnym obliczeniom. Przy jej stosowaniu nie
występują błędy zaokrągleń.
W literaturze przedmiotu opisano wiele podejść do dokładnej arytmetyki liczb
rzeczywistych, z zastosowaniem ułamków łańcuchowych [3, 4], skalowanych liczb
całkowitych [5, 6], przekształceń Möbiusa [7], i innych sposobów, które nie doczekały
się praktycznych implementacji, jak układy pozycyjne o niecałkowitej lub ujemnej
podstawie, zagnieżdżone ciągi przedziałów o końcach wymiernych, czy ciągi Cauchy’ego [8]. Niniejsza praca przedstawia użycie do dokładnych obliczeń ułamków
łańcuchowych, co po raz pierwszy zaproponował Gosper [3].
2. Ułamki łańcuchowe
Skończony ułamek łańcuchowy to wyrażenie postaci
(1)
1
a0 +
1
a1 +
1
a2 +
a3 +
1
..
. + ak
Słowa kluczowe. Dokładna arytmetyka liczb rzeczywistych, ułamki łańcuchowe, arytmetyka Gospera, szereg Ostrogradskiego-Sierpińskiego, leniwe wartościowanie, język programowania
Python.
1
gdzie ogniwo a0 jest liczbą całkowitą, a kolejne ogniwa ai dla i > 0 to liczby
naturalne dodatnie, przy czym ostatnie ogniwo ak jest większe od 1 [9, rozdz. XII].
Dla zaoszczędzenia miejsca zamiast wyrażenia (1) pisze się często
[a0 ; a1 , a2 , a3 , . . . , ak ].
Nieskończony ułamek łańcuchowy to granica ciągu ułamków skończonych
[a0 ; a1 , a2 , a3 , . . .] = lim [a0 ; a1 , a2 , a3 , . . . , ak ].
k→∞
Każdą liczbę rzeczywistą można jednoznacznie przedstawić w postaci ułamka
łańcuchowego, przy czym liczbom wymiernym odpowiadają skończone ułamki łańcuchowe, a liczbom niewymiernym – nieskończone. Kolejne ogniwa ułamka łańcuchowego odpowiadającego danej liczbie wymiernej łatwo obliczać, stosując algorytm
Euklidesa wyznaczania największego wspólnego dzielnika, np.
96
=
1 · 65 + 31
65
=
2 · 31 + 3
31
3
=
=
10 · 3 + 1
3 · 1,
a zatem
96
= [1; 2, 10, 3].
65
Oto rozwinięcia niektórych liczb niewymiernych w ułamki łańcuchowe:
√
2 = [1; 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, . . .]
√
− 2 = [−2; 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, . . .]
√
3 = [1; 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, . . .]
e = [2; 1, 2, 1, 1, 4, 1, 1, 6, 1, 1, 8, . . .]
e1/2
=
[1; 1, 1, 1, 5, 1, 1, 9, 1, 1, 13, 1, . . .]
tg(1/2) = [0; 1, 1, 4, 1, 8, 1, 12, 1, 16, 1, 20, . . .]
π = [3; 7, 15, 1, 292, 1, 1, 1, 2, 1, 3, 1, . . .]
√
3
2 = [1; 3, 1, 5, 1, 1, 4, 1, 1, 8, 1, 14, . . .].
Atrakcyjną własnością ułamków łańcuchowych jest to, że obcięte ułamki łańcuchowe (redukty) stanowią najlepsze możliwe przybliżenia liczb rzeczywistych. Po
obliczeniu wartości początkowych ogniw rozwinięcia danej liczby rzeczywistej znana
jest dokładność jej przybliżenia przez to rozwinięcie. Jest to spostrzeżenie kluczowe
dla dalszej części pracy, w której przybliżane liczby są wynikami działań na innych
ułamkach łańcuchowych.
3. Wymagane cechy języka programowania
Do obliczeń na ułamkach łańcuchowych zgodnych z algorytmami przedstawionymi poniżej potrzebne są tylko liczby całkowite, jednak ich wartość bezwzględna
może być dowolnie duża, większa niż pojemność słowa maszynowego. Aby zatem
móc w jakimś języku programowania wygodnie zaimplementować podane algorytmy, powinien być w nim dostępny typ całkowity o nieograniczonym zbiorze dopuszczalnych wartości. Wymaganie to spełnia język Python – od zawsze można w nim
było korzystać z typu wbudowanego long, a od wersji 2.3, wydanej w 2003 roku,
został on w pełni zunifikowany z typem int, odpowiadającym słowu maszynowemu.
Oznacza to, że wyrażenia całkowite, których wartość wykracza poza zakres typu
int, są automatycznie konwertowane do typu long.
2
Sposób wykonywania obliczeń w opracowanej bibliotece różni się od zwykłego, gorliwego wartościowania (ang. eager evaluation). Opiera się on mianowicie na
leniwym wartościowaniu (ang. lazy evaluation): przy tworzeniu wyrażenia arytmetycznego nie są wykonywane żadne obliczenia na liczbach. Zamiast tego powstaje
skierowany acykliczny graf podwyrażeń. Od wierzchołka tego grafu, odpowiadającego całemu wyrażeniu, można następnie kolejno żądać tyle ogniw bądź cyfr dziesiętnych, ile zachodzi potrzeba. Wierzchołek ten z kolei żąda ogniw z wierzchołków
z nim połączonych, które odpowiadają jego bezpośrednim podwyrażeniom, i tak
dalej, aż do wierzchołków wejściowych, odpowiadających stałym liczbom. Dzięki
leniwemu wartościowaniu wykonuje się tylko tyle obliczeń, ile jest w danej chwili
potrzebne, a podwyrażenia dynamicznie dopasowują swoją dokładność, aby cyfry
otrzymywane z wartościowania całego wyrażenia zawsze były prawidłowe.
Istnieją języki programowania, np. Haskell [10], oparte w całości na koncepcji
leniwego wartościowania, ale i w języku Python można z niego wygodnie korzystać
dzięki generatorom, wprowadzonym do niego eksperymentalnie w wersji 2.2 i na
stałe w wersji 2.3. Generatory są podobne do funkcji, zwracających tablice, lecz
w odróżnieniu od nich przy kolejnych wywołaniach zwracają wynik element po elemencie. Dzięki temu funkcja wywołująca generator może rozpocząć przetwarzanie
wyniku nie czekając, aż zostanie on w całości wygenerowany, co zresztą byłoby
w ogóle niemożliwe, jeśli generator jest nieskończony.
Ponieważ tworzenie wyrażeń arytmetycznych wiąże się z powstaniem grafu powiązanych zależnościami obiektów, konieczne jest zwalnianie pamięci, zajmowanej
przez obiekt, gdy przestanie on być potrzebny, czyli przestaną być potrzebne wszystkie obiekty, które korzystały z wyników przezeń generowanych. Zarządzanie pamięcią znacznie się upraszcza przy zastosowaniu odśmiecania pamięci (ang. garbage
collection), czyli automatycznego zwalniania niepotrzebnych obszarów pamięci. Język Python jest od swej pierwszej wersji wyposażony w automatyczny odśmiecacz
pamięci.
4. Działania arytmetyczne
Algorytmy czterech działań arytmetycznych na leniwie obliczanych ułamkach
łańcuchowych podał Gosper [3, pkt. 101]. Polegają one na sprowadzeniu sum, różnic, iloczynów i ilorazów do ogólnej funkcji bihomograficznej, której kolejne ogniwa
mogą być generowane na podstawie kolejnych – generowanych na żądanie – ogniw
obu operandów.
Każdy system dokładnej arytmetyki liczb rzeczywistych napotyka problem tego,
że równość liczb niewymiernych jest nierozstrzygalna w skończonej liczbie kroków.
Dotyczy to też opisywanego w tej pracy systemu: jeśli dowolnie wiele początkowych
ogniw dwóch nieskończonych ułamków łańcuchowych jest parami równe, to nie
wiadomo, czy kolejne ogniwa będą dalej parami równe. Problem nierostrzygalny
występuje nie tylko przy porównywaniu liczb, ale i wtedy, gdy argumenty operacji
są niewymierne, a jej wynik prawdopodobnie wymierny.
Możliwe jest idealistyczne rozwiązanie tego problemu: udokumentowanie faktu,
że takie obliczenia nigdy się nie skończą. Autor umożliwił taką wersję działania
opracowanej biblioteki – osiąga się ją przez ustawienie parametru kmax , dostępnego dla programisty, na wartość ujemną. Domyślne ustawienie tego parametru jest
jednak inne; autor sądzi, że bardziej praktyczne. Otóż jeśli algorytm Gospera wykona kmax iteracji z jednakowymi wartościami dolnego i górnego ograniczenia na
następne generowane ogniwo, to generuje owo górne ograniczenie, a po nim symbol
None, oznaczający nieskończone ogniwo, czyli koniec ułamka łańcuchowego. Podobnie jeśli funkcja służąca do porównywania liczb pobierze po kmax /2 parami równych
ogniw z obu swoich argumentów, to decyduje, że są one równe.
3
Algorytm Gospera potrzebuje 81 iteracji, aby odkryć, że
√
(math.sqrt(5) − 1)/2 − ( 5 − 1)/2 > 0,
gdzie math.sqrt() to funkcja obliczająca pierwiastek jako liczbę zmiennoprzecinkową, a funkcji porównującej liczby potrzeba 39 = 78/2 iteracji, by odkryć, że
powyższa odjemna i odjemnik są nierówne.
√
Jest to przypadek pesymistyczny:
każde ogniwo liczby ( 5 − 1)/2 odpowiada za
√
przyrost dokładności o log10 ( 5 + 1)/2 ≈ 0,21 cyfr dziesiętnych, jednak twierdzenie
Lochsa [11] głosi, że w prawie każdym (w sensie Lebesgue’a) ułamku łańcuchowym
każde kolejne ogniwo powoduje przyrost dokładności średnio o π 2 /(6 ln 2 ln 10) ≈
1,03 cyfry dziesiętnej. Badania empiryczne [12] wskazują, że wariancja liczby cyfr
dziesiętnych przypadających na jedno ogniwo wynosi w przybliżeniu 0,62. Po kmax =
100 iteracjach z niezmienionymi wartościami dolnego i górnego ograniczenia na kolejne ogniwo liczba poprawnych cyfr po przecinku w wartości nie wygenerowanego
jeszcze ogona ułamka łańcuchowego ma rozkład w przybliżeniu normalny z wartością oczekiwaną 1,03 × 100 i wariancją 0,62 × 100. Zatem różnica między dokładną
wartością ogona a wygenerowaną liczbą całkowitą ma rozkład w przybliżeniu logarytmiczno-normalny o wartości oczekiwanej
10−1,03×100+(0,62×100 ln 10)/2 ≈ 2,07−100 ,
czyli z grubsza pomiędzy 10−32 a 10−31 .
Algorytm generuje wtedy parę ogniw (a, ∞), podczas gdy poprawny wynik to
ciąg (a, a0 , . . .) lub (a − 1, 1, a00 , . . .), gdzie a0 ­ 1031 , a00 ­ 1031 − 1. Zgodnie z twierdzeniem Gaussa-Kuźmina [13, 14] dowolne ogniwo w rozwinięciu prawie każdej
liczby na ułamek łańcuchowy jest większe od n0 = 1031 z prawdopodobieństwem
log2 (1 + 1/n0 ) = log2 (1 + 10−31 ) ≈ 10−31 / ln 2 ≈ 1,5 × 10−31 .
Zatem przy kmax = 100 prawdopodobieństwo, że algorytm błędnie obetnie rozwinięcie losowej liczby w ułamek łańcuchowy nie przekracza wartości 1,5 × 10−31
pomnożonej przez liczbę obliczonych ogniw.
Podobny problem występuje, gdy funkcja o argumentach niewymiernych ma wymierny wynik, np. eln 5 , czy cos(π/3). Został on podobnie rozwiązany w opracowanej
bibliotece.
Na rysunku 1 przedstawiono sprawdzenie kilku przykładowych tożsamości, wykonane w trybie interaktywnym interpretera Pythona z zastosowaniem opracowanej biblioteki, a możliwe właśnie dzięki decyzji projektowej objaśnionej powyżej.
W przedostatnim przykładzie porównywane liczby są nierówne, gdyż liczby zmiennoprzecinkowe to w gruncie rzeczy liczby wymierne o dużym mianowniku, a zatem
skończone ułamki łańcuchowe, które mają mniej niż kmax ogniw. W ostatnim przykładzie biblioteka sygnalizuje równość porównywanych liczb, ponieważ reprezentują
je nieskończone ułamki łańcuchowe, z których porównano tylko kmax początkowych
ogniw.
4
>>> (sqrt(2) - 1)*(sqrt(2) + 1)
1.
>>> sqrt(5 + 2*sqrt(6)) == sqrt(2) + sqrt(3)
True
>>> [sin(x)**2 + cos(x)**2 for x in
... [pi*random(), pi*random(), pi*random()]]
[1., 1., 1.]
>>> [cos(3*x)/cos(x) == cos(x)**2 - 3*sin(x)**2 for x in
... [pi*random(), pi*random(), pi*random()]]
[True, True, True]
>>> [x == x + cf(10)**-1000 for x in
... [cf(random()), cf(random()), cf(random())]]
[False, False, False]
>>> sqrt(2) == sqrt(2) + cf(10)**-1000
True
Rysunek 1. Przykładowe tożsamości i wyniki ich sprawdzenia
5. Funkcje przestępne i π
Do funkcji przestępnych zalicza się funkcję wykładniczą i logarytmiczną oraz
funkcje trygonometryczne i cyklometryczne. Nie wszystkie z nich trzeba było bezpośrednio implementować w opracowanej bibliotece, a to dzięki poniższym tożsamościom:
x . x
sin x =
2 tg
1 + tg2
2
2
.
x
2 x
cos x =
1 − tg
1 + tg2
2
2
x
arc sin x = arc tg √
1 − x2
arc cos x = π/2 − arc sin x
xy
= ey ln x
log10 x =
ln x/ ln 10.
Po uwzględnieniu tych tożsamości, aby uzyskać zastępnik standardowej biblioteki
math, pozostaje zdefiniować funkcje sqrt(), exp(), tan(), log(), atan() oraz
liczbę pi.
Leniwy algorytm obliczania pierwiastka kwadratowego oparto na metodzie Newtona (stycznych), dzięki czemu jest on szybszy, niż przy zastosowaniu tożsamości
x1/2 = e(ln x)/2 . Algorytm rozpoczyna
się od obliczenia wartości a, równej całko√
witoliczbowemu przybliżeniu x. Tak długo, jak ogniwa liczb a i x/a są równe,
należy je emitować; w przeciwnym wypadku należy obliczyć następne przybliżenie
a := (a + x/a)/2. Zbieżność tego algorytmu jest kwadratowa, co oznacza, że po każdym kolejnym wyznaczeniu wartości a liczba poprawnych cyfr dziesiętnych wyniku
w przybliżeniu się podwaja.
Algorytmy obliczania wartości pozostałych funkcji przestępnych autor oparł na
rozwijaniu argumentu w szereg Ostrogradskiego drugiego rodzaju [15], który poprawniej powinien się zwać szeregiem Ostrogradskiego-Sierpińskiego [16]:
x = bxc + 1/q1 − 1/q2 + 1/q3 − 1/q4 + . . . ,
gdzie kolejne mianowniki qk wybiera się zachłannie największe z możliwych, przy
zachowaniu tego, by kolejne sumy częściowe szeregu przybliżały x na przemian
z dołu i z góry.
5
Po składniku ±1/q kolejny składnik jest co do wartości bezwzględnej równy co
najwyżej 1/(q 2 + q), zatem liczba poprawnych cyfr w przybliżeniu co najmniej
się podwaja z każdym składnikiem. Przypadkiem pesymistycznym jest rozwijanie
w szereg Ostrogradskiego-Sierpińskiego liczb o części ułamkowej równej stałej Cahena 1/1 − 1/2 + 1/6 − 1/42 + 1/1806 − 1/3263442 + 1/10650056950806 − . . .
Leniwe obliczanie wartości funkcji ex polega na leniwym rozwijaniu x w szereg Ostrogradskiego-Sierpińskiego z pamiętaniem dwóch ostatnich sum częściowych
sk−1 i sk oraz zastosowaniu poniższych zależności do obliczania esk−1 i esk :
e1/q = [1; q − 1, 1, 1, 3q − 1, 1, 1, 5q − 1, . . .],
ex+y = ex ey ,
ex−y = ex /ey .
Tak długo, jak ogniwa liczb esk−1 i esk są równe, należy je emitować; w przeciwnym
przypadku należy obliczyć następną sumę częściową sk+1 .
Algorytm leniwego obliczania wartości tg x jest analogiczny, z tym, że korzysta
z zależności
tg(1/q) = [0; q − 1, 1, 3q − 2, 1, 5q − 2, 1, 7q − 2, . . .],
tg x ± tg y
.
1 ∓ tg x tg y
Wartość ln x oblicza się leniwie, leniwie znajdując sumy częściowe szeregu Ostrogradskiego-Sierpińskiego tak, żeby
tg(x ± y) =
esk−1 < x ¬ esk
lub esk−1 > x ­ esk .
Tak długo, jak ogniwa liczb sk−1 i sk są równe, należy je emitować; w przypadku
ich nierówności – obliczyć następną sumę częściową sk+1 .
Wartości funkcji arc tg x oblicza się tak samo, jak logarytmu naturalnego, tylko
z tg sk−1 i tg sk zamiast esk−1 i esk .
Kwadratowa zbieżność powyższych algorytmów wynika z kwadratowej zbieżności
szeregu Ostrogradskiego-Sierpińskiego.
Liczbę π oblicza się leniwie na podstawie uogólnionego ułamka łańcuchowego
1
π =3+
9
6+
6+
25
49
6+
81
6+
..
.
Algorytm ten ma zbieżność liniową, ale okazał się w praktyce szybszy od algorytmów o wyższym rzędzie zbieżności.
6. Podsumowanie
Autor opracował bibliotekę dokładnych obliczeń na liczbach rzeczywistych w języku programowania Python, opartą na leniwie wartościowanych ułamkach łańcuchowych. Z adresu WWW http://sun.aei.polsl.pl/~mciura/cf.py można
pobrać kod źródłowy biblioteki.
Działania arytmetyczne w opracowanej bibliotece opierają się na algorytmie Gospera, przy czym autor wprowadził doń heurystykę, przerywającą pętle nieskończone. Wartości funkcji przestępnych są obliczane przez autorskie algorytmy, oparte
na rozwijaniu liczb w szereg Ostrogradskiego-Sierpińskiego.
6
Opracowana biblioteka może znaleźć zastosowanie przy badaniu zjawisk chaotycznych, w geometrii obliczeniowej i numerycznej teorii liczb, a także w programach edukacyjnych. Szybkość wykonywania działań arytmetycznych jest zadowalająca, natomiast czas potrzebny do obliczenia wyników bardziej skomplikowanych
wyrażeń zawierających funkcje przestępne można liczyć nawet w sekundach.
Literatura
[1] G. van Rossum, F.L. Drake, Jr. (ed.), The Python Language Reference Manual (version 2.5),
Network Theory Ltd, 2006.
[2] The GMP Team, The GNU Multiple Precision Arithmetic Library, Free Software Foundation,
2007.
[3] M. Minsky, B. Gosper, M. Beeler et al., Artificial Intelligence Memo No. 239, Massachusetts
Institute of Technology, A.I. Laboratory, 1972.
[4] J. Vuillemin, Exact real computer arithmetic with continued fractions, Proceedings of the
1988 ACM conference on LISP and functional programming, ACM, 1988, 14–27.
[5] H.-J. Boehm, R. Cartwright, M. Riggle, M.J. O’Donnell, Exact real arithmetic: a case study in
higher order programming, Proceedings of the 1986 ACM conference on LISP and functional
programming, ACM, 1986, 162–173.
[6] K. Briggs, Implementing exact real arithmetic in Python, C++ and C, Theoretical Computer
Science 351 (2006), 74–81.
[7] P. Potts, Exact real arithmetic using Möbius transformations, rozprawa doktorska, Department of Computing, Imperial College of Science, Technology and Medicine, University of
London, 1999.
[8] M. Escardó, Introduction to Exact Numerical Computation, School of Computer Science,
St. Andrews University, 2000.
[9] W. Sierpiński, Teoria liczb, PWN, 1950.
[10] S. Peyton Jones (red.), Haskell 98 language and libraries: the Revised Report, Cambridge
University Press, 2003.
[11] G. Lochs, Vergleich der Genauigkeit von Dezimalbruch und Kettenbruch, Abhandlungen aus
dem Mathematischen Seminar der Universität Hamburg 27 (1964), 142–144.
[12] W. Bosma, K. Dajani, C. Kraaikamp, Entropy and counting correct digits, Raport nr 9925,
Department of Mathematics, University of Nijmegen, 1999.
[13] C.F. Gauß, Werke, tom 101 , 552–556.
[14] R.O. Kuzmin, Sur un problème de Gauss, Atti del Congresso Internazionale dei Matematici,
Bologna, 1928, 83–89.
[15] J.J. Remez, O zakonomiernych riadach, kotoryje mogut byt’ swiazany s dwumia ałgoritmami
M. W. Ostrogradskogo dla pribliżenija irracyonalnych czisieł, Uspiechi matiematiczeskich
nauk 6 (1951), 33–42.
[16] W. Sierpiński, O kilku algorytmach dla rozwijania liczb rzeczywistych na szeregi, Sprawozdania z posiedzeń Towarzystwa Naukowego Warszawskiego, Wydział III 4 (1911), 56–77.
Instytut Informatyki, Politechnika Śląska, 44-100 Gliwice
Adres poczty elektronicznej: [email protected]
7
Download