To było pytanie, które zadałem mi na ostatnim wywiadzie i chcę wiedzieć (tak naprawdę nie pamiętam teorii analizy numerycznej, więc proszę o pomoc :)
Jeśli mamy jakąś funkcję, która gromadzi liczby zmiennoprzecinkowe:
std::accumulate(v.begin(), v.end(), 0.0);
v
jest std::vector<float>
na przykład.
Czy lepiej byłoby posortować te liczby przed ich zgromadzeniem?
Która kolejność daje najbardziej precyzyjną odpowiedź?
Podejrzewam, że sortowania liczb w kolejności rosnącej faktycznie sprawiają, że błąd liczbowy mniej , ale niestety nie mogę tego udowodnić sobie.
PS Zdaję sobie sprawę, że to prawdopodobnie nie ma nic wspólnego z programowaniem w prawdziwym świecie, po prostu jestem ciekawy.
c++
floating-point
precision
Yippie-Ki-Yay
źródło
źródło
Odpowiedzi:
Twój instynkt ma w zasadzie rację, sortowanie w rosnącym porządku (wielkości) zwykle nieco poprawia sytuację. Rozważmy przypadek, w którym dodajemy zmiennoprzecinkowe pojedynczej precyzji (32-bitowe) i mamy 1 miliard wartości równych 1 / (1 miliard) i jedną wartość równą 1. Jeśli 1 pojawi się jako pierwsza, to suma przyjdzie do 1, ponieważ 1 + (1/1 miliarda) to 1 z powodu utraty precyzji. Każdy dodatek nie ma żadnego wpływu na całość.
Jeśli małe wartości pojawią się pierwsze, to przynajmniej zsumują się do czegoś, chociaż nawet wtedy mam ich 2 ^ 30, a po około 2 ^ 25 wracam do sytuacji, w której każda z osobna nie wpływa na sumę już więcej. Więc nadal będę potrzebować więcej sztuczek.
To skrajny przypadek, ale generalnie dodanie dwóch wartości o podobnej wielkości jest dokładniejsze niż dodanie dwóch wartości o bardzo różnych wielkościach, ponieważ w ten sposób „odrzuca się” mniej bitów precyzji z mniejszej wartości. Sortując liczby, grupujesz razem wartości o podobnej wielkości, a dodając je w porządku rosnącym, dajesz małym wartościom „szansę” na skumulowane osiągnięcie wielkości większych liczb.
Jednak jeśli chodzi o liczby ujemne, łatwo jest „przechytrzyć” to podejście. Rozważmy trzy wartości w sumie
{1, -1, 1 billionth}
. Suma poprawna arytmetycznie to1 billionth
, ale jeśli moje pierwsze dodanie obejmuje małą wartość, wtedy moja suma końcowa wyniesie 0. Z 6 możliwych zamówień tylko 2 są „poprawne” -{1, -1, 1 billionth}
i{-1, 1, 1 billionth}
. Wszystkie 6 rzędów dają wyniki, które są dokładne w skali największej wartości na wejściu (0,0000001% na zewnątrz), ale dla 4 z nich wynik jest niedokładny w skali rzeczywistego rozwiązania (100% poza). Konkretny problem, który rozwiązujesz, powie ci, czy ten pierwszy jest wystarczająco dobry, czy nie.W rzeczywistości możesz grać o wiele więcej sztuczek, niż tylko dodawać je w posortowanej kolejności. Jeśli masz wiele bardzo małych wartości, średnią liczbę średnich wartości i niewielką liczbę dużych wartości, najdokładniejsze może być najpierw zsumowanie wszystkich małych, a następnie osobne zsumowanie średnich i dodanie tych dwóch sum razem, a następnie dodaj duże. Znalezienie najdokładniejszej kombinacji dodawań zmiennoprzecinkowych nie jest wcale trywialne, ale aby poradzić sobie z naprawdę złymi przypadkami, możesz zachować cały szereg bieżących sum o różnych wielkościach, dodać każdą nową wartość do sumy, która najlepiej pasuje do jej wielkości, a kiedy suma bieżąca zacznie być zbyt duża dla swojej wielkości, dodaj ją do następnej sumy i rozpocznij nową. Doprowadzony do jego logicznego ekstremum, proces ten jest równoważny wykonaniu sumy w typie z dowolną precyzją (więc zrób to). Biorąc jednak pod uwagę uproszczony wybór dodawania rosnącego lub malejącego rzędu wielkości, zwiększanie jest lepszym rozwiązaniem.
Ma to pewien związek z programowaniem w świecie rzeczywistym, ponieważ istnieją przypadki, w których obliczenia mogą pójść bardzo źle, jeśli przypadkowo odetniesz „ciężki” ogon składający się z dużej liczby wartości, z których każda jest zbyt mała, aby mieć na nią indywidualny wpływ suma lub jeśli odrzucisz zbyt dużą precyzję z wielu małych wartości, które indywidualnie wpływają tylko na kilka ostatnich bitów sumy. W przypadkach, gdy ogon i tak jest znikomy, prawdopodobnie nie obchodzi cię to. Na przykład, jeśli na początku dodajesz tylko niewielką liczbę wartości i używasz tylko kilku znaczących cyfr z sumy.
źródło
Istnieje również algorytm przeznaczony do tego rodzaju operacji akumulacji, zwany sumowaniem Kahana , o którym prawdopodobnie powinieneś wiedzieć.
Według Wikipedii
źródło
sum
ic
różniących wielkości. Można go w trywialny sposób rozszerzyć na N zmiennych.-ffast-math
na GCC).-ffast-math
. Z tej dyskusji i tego linku dowiedziałem się , że jeśli zależy ci na dokładności numerycznej, prawdopodobnie powinieneś unikać używania,-ffast-math
ale to w wielu aplikacjach, w których możesz być związany z procesorem, ale nie dbasz o dokładne obliczenia numeryczne (na przykład programowanie gier ),-ffast-math
jest rozsądne w użyciu. W związku z tym chciałbym poprawić mój mocno sformułowany „zakazany” komentarz.sum, c, t, y
będzie użycie zmiennych o podwójnej precyzji dla . Musisz również dodaćsum -= c
wcześniejreturn sum
.Wypróbowałem skrajny przykład w odpowiedzi udzielonej przez Steve'a Jessopa.
Otrzymałem następujący wynik:
Błąd w pierwszej linii jest ponad dziesięciokrotnie większy w drugiej.
Jeśli zmienię
double
s nafloat
sw powyższym kodzie, otrzymam:Żadna z odpowiedzi nie jest nawet bliska 2,0 (ale druga jest nieco bliżej).
Używając sumowania Kahana (ze
double
s), jak opisał Daniel Pryden:Dostaję dokładnie 2,0:
I nawet jeśli zmienię
double
s nafloat
sw powyższym kodzie, otrzymam:Wydawałoby się, że Kahan to najlepsza droga!
źródło
double
nie cierpi źle utrata precyzji w dodaniu miliardowych części, ponieważ ma 52 znaczące bity, podczas gdy IEEEfloat
ma tylko 24 i będzie.c
aby zawierały wartości znacznie większe niż następny szczyt. Oznacza to, że suma jest dużo, dużo mniejsza niż główna suma, więc będzie ich bardzo dużo, aby dodać do siebie dużo. Zwłaszcza wdouble
arytmetyce.Istnieje pewna klasa algorytmów, które rozwiązują dokładnie ten problem, bez konieczności sortowania lub zmiany kolejności danych .
Innymi słowy, sumowanie można wykonać jednym przejściem po danych. To sprawia, że takie algorytmy mają zastosowanie w sytuacjach, gdy zbiór danych nie jest z góry znany, np. Gdy dane docierają w czasie rzeczywistym i trzeba zachować sumę bieżącą.
Oto streszczenie ostatniego artykułu:
Źródło: Algorytm 908: Dokładne sumowanie online strumieni zmiennoprzecinkowych .
źródło
Opierając się na odpowiedzi Steve'a, aby najpierw posortować liczby w porządku rosnącym, przedstawiłbym jeszcze dwa pomysły:
Zdecyduj się na różnicę w wykładniku dwóch liczb, powyżej której możesz zdecydować, że stracisz zbyt dużą precyzję.
Następnie dodaj liczby w kolejności, aż wykładnik akumulatora będzie zbyt duży dla następnej liczby, a następnie umieść akumulator w tymczasowej kolejce i rozpocznij akumulator z następną liczbą. Kontynuuj, aż wyczerpiesz oryginalną listę.
Powtarzasz ten proces z tymczasową kolejką (po posortowaniu) i prawdopodobnie większą różnicą wykładnika.
Myślę, że będzie to dość powolne, jeśli będziesz musiał przez cały czas obliczać wykładniki.
Szybko przeszedłem z programem i wynik był 1,99903
źródło
Myślę, że możesz zrobić coś lepszego niż sortowanie liczb, zanim je zbierzesz, ponieważ podczas procesu akumulacji akumulator staje się coraz większy. Jeśli masz dużą liczbę podobnych liczb, szybko zaczniesz tracić precyzję. Oto, co proponuję zamiast tego:
Oczywiście ten algorytm będzie najbardziej efektywny z kolejką priorytetową zamiast listy. Kod C ++:
kierowca:
Liczby w kolejce są ujemne, ponieważ
top
daje największą liczbę, ale chcemy najmniejszą . Mogłem podać więcej argumentów szablonu do kolejki, ale takie podejście wydaje się prostsze.źródło
To nie do końca odpowiada na twoje pytanie, ale mądrą rzeczą jest dwukrotne obliczenie sumy, raz w trybie zaokrąglania „zaokrąglenie w górę” i raz za pomocą „zaokrąglenia w dół”. Porównaj te dwie odpowiedzi, a wiesz, / jak / niedokładne są wyniki, i jeśli w związku z tym musisz użyć sprytniejszej strategii sumowania. Niestety, większość języków nie sprawia, że zmiana trybu zaokrąglania zmiennoprzecinkowego jest tak łatwa, jak powinna, ponieważ ludzie nie wiedzą, że jest on faktycznie przydatny w codziennych obliczeniach.
Spójrz na arytmetykę interwałową, w której wykonujesz wszystkie obliczenia matematyczne w ten sposób, zachowując najwyższe i najniższe wartości. Prowadzi to do interesujących wyników i optymalizacji.
źródło
Najprostszym sortowaniem poprawiającym dokładność jest sortowanie według rosnącej wartości bezwzględnej. Dzięki temu najmniejsze wartości wielkości mają szansę na akumulację lub anulowanie przed interakcją z większymi wartościami wielkości, które spowodowałyby utratę precyzji.
To powiedziawszy, możesz zrobić to lepiej, śledząc wiele nienakładających się sum częściowych. Oto artykuł opisujący technikę i przedstawiający dowód dokładności: www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps
Ten algorytm i inne podejścia do dokładnego sumowania zmiennoprzecinkowego są zaimplementowane w prostym Pythonie pod adresem : http://code.activestate.com/recipes/393090/ Przynajmniej dwa z nich można w trywialny sposób przekonwertować na C ++.
źródło
W przypadku numerów IEEE 754 o pojedynczej lub podwójnej precyzji lub w znanym formacie, inną alternatywą jest użycie tablicy liczb (przekazywanych przez wywołującego lub w klasie dla C ++) indeksowanych przez wykładnik. Podczas dodawania liczb do tablicy dodawane są tylko liczby z tym samym wykładnikiem (do momentu znalezienia pustego pola i zapisania liczby). Gdy żądana jest suma, tablica jest sumowana od najmniejszej do największej, aby zminimalizować obcięcie. Przykład pojedynczej precyzji:
przykład podwójnej precyzji:
źródło
Twoje pływaki powinny być dodawane z podwójną precyzją. Zapewni to większą precyzję niż jakakolwiek inna technika. Aby uzyskać nieco większą precyzję i znacznie większą prędkość, możesz utworzyć powiedzmy cztery sumy i zsumować je na końcu.
Jeśli dodajesz liczby podwójnej precyzji, użyj long double jako sumy - jednak będzie to miało pozytywny wpływ tylko w implementacjach, w których long double faktycznie ma większą precyzję niż double (zazwyczaj x86, PowerPC w zależności od ustawień kompilatora).
źródło
Jeśli chodzi o sortowanie, wydaje mi się, że jeśli spodziewasz się anulowania, liczby powinny być dodawane w porządku malejącym , a nie rosnącym. Na przykład:
((-1 + 1) + 1e-20) da 1e-20
ale
((1e-20 + 1) - 1) da 0
W pierwszym równaniu dwie duże liczby są anulowane, podczas gdy w drugim człon 1e-20 zostaje utracony po dodaniu do 1, ponieważ nie ma wystarczającej precyzji, aby go zachować.
Ponadto sumowanie parami jest całkiem przyzwoite do sumowania wielu liczb.
źródło