Dlaczego (inf + 0j) * 1 jest przeliczane na inf + nanj?


97
>>> (float('inf')+0j)*1
(inf+nanj)

Czemu? Spowodowało to paskudny błąd w moim kodzie.

Dlaczego 1tożsamość multiplikatywna nie jest dawaniem (inf + 0j)?


1
Myślę, że słowo kluczowe, którego szukasz, to „ pole ”. Dodawanie i mnożenie są domyślnie definiowane w pojedynczym polu, aw tym przypadku jedynym standardowym polem, które może pomieścić twój kod, jest pole liczb zespolonych, więc obie liczby muszą być domyślnie traktowane jako liczby zespolone, zanim operacja będzie dobrze - zdefiniowane. Co nie znaczy, że nie mogli rozszerzyć tych definicji, ale najwyraźniej po prostu poszli ze standardem i nie odczuwali potrzeby, aby robić wszystko, co w ich mocy, aby rozszerzyć definicje.
user541686

1
Aha, i jeśli uważasz, że te dziwactwa są frustrujące i chcesz uderzyć komputer w komputer, masz moje współczucie .
user541686

2
@Mehrdad po dodaniu tych elementów nie skończonych przestaje być polem. Rzeczywiście, skoro nie ma już multiplikatywnej neutralności, z definicji nie może być polem.
Paul Panzer,

@PaulPanzer: Tak, myślę, że po prostu wrzucili później te elementy.
user541686

1
liczby zmiennoprzecinkowe (nawet jeśli wykluczysz nieskończoność i NaN) nie są polami. Większość tożsamości, które przechowują dla pól, nie obsługuje liczb zmiennoprzecinkowych.
plugwash

Odpowiedzi:


95

1Przekształca się liczbę zespoloną pierwszy 1 + 0j, co następnie prowadzi do inf * 0rozmnażania, w wyniku nan.

(inf + 0j) * 1
(inf + 0j) * (1 + 0j)
inf * 1  + inf * 0j  + 0j * 1 + 0j * 0j
#          ^ this is where it comes from
inf  + nan j  + 0j - 0
inf  + nan j

8
Jeśli chodzi o odpowiedź na pytanie „dlaczego…?”, Prawdopodobnie najważniejszym krokiem jest ten pierwszy, gdzie 1jest rzucane 1 + 0j.
Warren Weckesser,

5
Zauważ, że C99 określa, że ​​rzeczywiste typy zmiennoprzecinkowe nie są promowane do złożonego, gdy są mnożone przez typ złożony (sekcja 6.3.1.8 projektu standardu) i, o ile wiem, to samo dotyczy std :: complex w C ++. Może to częściowo wynikać z wydajności, ale pozwala również uniknąć niepotrzebnych NaN.
benrg

@benrg W NumPy array([inf+0j])*1również zwraca array([inf+nanj]). Zakładając, że faktyczne mnożenie ma miejsce gdzieś w kodzie C / C ++, czy oznaczałoby to, że napisali niestandardowy kod, aby emulować zachowanie CPythona, zamiast używać _Complex lub std :: complex?
marnix

1
@marnix to coś więcej niż to. numpyma jedną klasę centralną, ufuncz której wywodzi się prawie każdy operator i funkcja. ufuncdba o rozgłaszanie, zarządzając krokami wszystkich tych podstępnych administratorów, które sprawiają, że praca z tablicami jest tak wygodna. Dokładniej, podział pracy między konkretnego operatora a maszynę ogólną polega na tym, że określony operator implementuje zestaw „najbardziej wewnętrznych pętli” dla każdej kombinacji typów elementów wejściowych i wyjściowych, które chce obsługiwać. Ogólna maszyneria dba o wszelkie zewnętrzne pętle i wybiera najlepszą wewnętrzną pętlę meczu ...
Paul Panzer

1
... promowanie niezupełnie pasujących typów zgodnie z wymaganiami. Możemy uzyskać dostęp do listy dostarczonych pętli wewnętrznych za pośrednictwem typesatrybutu dla np.multiplytych plonów ['??->?', 'bb->b', 'BB->B', 'hh->h', 'HH->H', 'ii->i', 'II->I', 'll->l', 'LL->L', 'qq->q', 'QQ->Q', 'ee->e', 'ff->f', 'dd->d', 'gg->g', 'FF->F', 'DD->D', 'GG->G', 'mq->m', 'qm->m', 'md->m', 'dm->m', 'OO->O'], widzimy, że prawie nie ma typów mieszanych, w szczególności nie ma takich, które mieszają zmiennoprzecinkowe "efdg"ze złożonymi "FDG".
Paul Panzer,

32

Pod względem mechanicznym przyjęta odpowiedź jest oczywiście poprawna, ale uważam, że można udzielić głębszej odpowiedzi.

Po pierwsze, warto wyjaśnić pytanie, tak jak robi to @PeterCordes w komentarzu: „Czy istnieje tożsamość multiplikatywna dla liczb zespolonych, która działa na inf + 0j?” lub innymi słowy, jest tym, co OP widzi słabość w implementacji komputerowej złożonego mnożenia lub czy jest coś koncepcyjnie niewłaściwego zinf+0j

Krótka odpowiedź:

Używając współrzędnych biegunowych, możemy zobaczyć złożone mnożenie jako skalowanie i obrót. Obracając nieskończone „ramię” nawet o 0 stopni, jak w przypadku pomnożenia przez jeden, nie możemy oczekiwać, że umieścimy jego końcówkę ze skończoną precyzją. Tak więc, rzeczywiście, jest coś zasadniczo nie w porządku inf+0j, a mianowicie, że gdy tylko osiągniemy nieskończoność, skończone przesunięcie staje się bez znaczenia.

Długa odpowiedź:

Tło: „Wielką rzeczą”, wokół której obraca się to pytanie, jest kwestia rozszerzenia systemu liczb (myśl o liczbach rzeczywistych lub zespolonych). Jednym z powodów, dla których ktoś mógłby chcieć to zrobić, jest dodanie jakiejś koncepcji nieskończoności lub „ujednolicenie”, jeśli ktoś jest matematykiem. Są też inne powody ( https://en.wikipedia.org/wiki/Galois_theory , https://en.wikipedia.org/wiki/Non-standard_analysis ), ale nie jesteśmy nimi zainteresowani.

Zagęszczanie w jednym punkcie

Problem z takim rozszerzeniem polega oczywiście na tym, że chcemy, aby te nowe liczby pasowały do ​​istniejącej arytmetyki. Najprostszym sposobem jest dodanie pojedynczego elementu w nieskończoności ( https://en.wikipedia.org/wiki/Alexandroff_extension ) i uczynienie go równym wszystkim innym niż zero podzielone przez zero. Działa to w przypadku liczb rzeczywistych ( https://en.wikipedia.org/wiki/Projectively_extended_real_line ) i liczb zespolonych ( https://en.wikipedia.org/wiki/Riemann_sphere ).

Inne rozszerzenia ...

Chociaż jednopunktowe zagęszczenie jest proste i matematycznie poprawne, poszukiwano „bogatszych” rozszerzeń obejmujących wiele infintiesów. Standard IEEE 754 dla rzeczywistych liczb zmiennoprzecinkowych ma + inf i -inf ( https://en.wikipedia.org/wiki/Extended_real_number_line ). Wygląda naturalnie i prosto, ale już zmusza nas do skakania przez obręcze i wymyślania rzeczy takich jak -0 https://en.wikipedia.org/wiki/Signed_zero

... złożonej płaszczyzny

A co z więcej niż jednym rozszerzeniem inf płaszczyzny złożonej?

W komputerach liczby zespolone są zwykle implementowane przez sklejenie dwóch liczb rzeczywistych fp, jednej dla części rzeczywistej i jednej dla części urojonej. To jest w porządku, o ile wszystko jest skończone. Jednak gdy tylko uważa się, że nieskończoności sprawy stają się trudne.

Płaszczyzna zespolona ma naturalną symetrię obrotową, która dobrze łączy się ze złożoną arytmetyką, ponieważ pomnożenie całej płaszczyzny przez e ^ phij jest tym samym, co obrót fi radianu wokół 0.

Ta rzecz z załącznika G.

Teraz, aby wszystko było proste, złożone fp po prostu używa rozszerzeń (+/- inf, nan itd.) Podstawowej implementacji liczb rzeczywistych. Ten wybór może wydawać się tak naturalny, że nie jest nawet postrzegany jako wybór, ale przyjrzyjmy się bliżej, co on oznacza. Prosta wizualizacja tego przedłużenia płaszczyzny zespolonej wygląda następująco (I = nieskończony, f = skończony, 0 = 0)

I IIIIIIIII I
             
I fffffffff I
I fffffffff I
I fffffffff I
I fffffffff I
I ffff0ffff I
I fffffffff I
I fffffffff I
I fffffffff I
I fffffffff I
             
I IIIIIIIII I

Ale ponieważ prawdziwa złożona płaszczyzna to taka, która uwzględnia złożone mnożenie, projekcja byłaby bardziej pouczająca

     III    
 I         I  
    fffff    
   fffffff   
  fffffffff  
I fffffffff I
I ffff0ffff I
I fffffffff I
  fffffffff  
   fffffff   
    fffff    
 I         I 
     III    

W tej projekcji widzimy „nierównomierny rozkład” nieskończoności, który jest nie tylko brzydki, ale także źródłem problemów tego rodzaju, na jakie cierpiał OP: większość nieskończoności (tych z form (+/- inf, skończona) i (skończona, + / -inf) są skupione razem w czterech głównych kierunkach, wszystkie inne kierunki są reprezentowane przez tylko cztery nieskończoności (+/- inf, + -inf). Nie powinno dziwić, że rozszerzenie złożonego mnożenia na tę geometrię jest koszmarem .

Załącznik G specyfikacji C99 dokłada wszelkich starań, aby działał, w tym naginając zasady dotyczące sposobu infi naninterakcji (zasadniczo infatuty nan). Problem OP jest omijany przez nie promowanie rzeczywistych i proponowany typ czysto urojony na złożony, ale fakt, że prawdziwe 1 zachowuje się inaczej niż złożone 1, nie wydaje mi się rozwiązaniem. Co znamienne, w załączniku G brakuje pełnego określenia, jaki powinien być iloczyn dwóch nieskończoności.

Czy możemy zrobić lepiej?

Kusi, aby spróbować rozwiązać te problemy, wybierając lepszą geometrię nieskończoności. Analogicznie do wydłużonej linii rzeczywistej moglibyśmy dodać jedną nieskończoność dla każdego kierunku. Ta konstrukcja jest podobna do płaszczyzny rzutowej, ale nie skupia się w przeciwnych kierunkach. Nieskończoności byłyby reprezentowane we współrzędnych biegunowych inf xe ^ {2 omega pi i}, definiowanie iloczynów byłoby proste. W szczególności problem OP zostałby rozwiązany w sposób dość naturalny.

Ale na tym kończy się dobra wiadomość. W pewnym sensie możemy cofnąć się do punktu wyjścia jeden przez - nie bez powodu - wymaganie, aby nasze nieskończoności nowego stylu obsługiwały funkcje, które wydobywają ich rzeczywiste lub urojone części. Dodawanie to kolejny problem; dodając dwie nieantypodalne nieskończoności, musielibyśmy ustawić kąt na niezdefiniowany, tj. nan(można argumentować, że kąt musi leżeć między dwoma kątami wejściowymi, ale nie ma prostego sposobu na przedstawienie tej „częściowej nan-ności”)

Riemann na ratunek

W świetle tego wszystkiego być może stare dobre jednopunktowe zagęszczanie jest najbezpieczniejszą rzeczą do zrobienia. Być może autorzy Aneksu G czuli to samo, przypisując funkcję, cprojktóra łączy w sobie wszystkie nieskończoności.


Oto pokrewne pytanie, na które odpowiedziały osoby bardziej kompetentne w tej dziedzinie niż ja.


5
Tak, ponieważ nan != nan. Rozumiem, że ta odpowiedź jest pół żartem, ale nie rozumiem, dlaczego miałaby być pomocna dla OP w formie, w jakiej jest napisana.
cmaster

Biorąc pod uwagę, że kod w treści pytania w rzeczywistości nie używał ==(i biorąc pod uwagę, że zaakceptowali inną odpowiedź), wydaje się, że był to tylko problem ze sposobem wyrażenia tytułu w PO. Przeformułowałem tytuł, aby naprawić tę niespójność. (Celowe unieważnienie pierwszej połowy tej odpowiedzi, ponieważ zgadzam się z @cmaster: nie o to chodzi w tym pytaniu).
Peter Cordes

3
@PeterCordes to byłoby niepokojące, ponieważ używając współrzędnych biegunowych możemy postrzegać złożone mnożenie jako skalowanie i obrót. Obracając nieskończone „ramię” nawet o 0 stopni, jak w przypadku pomnożenia przez jeden, nie możemy oczekiwać umieszczenia jego końcówki z skończoną precyzją. Jest to moim zdaniem wyjaśnienie głębsze niż przyjęte, a także takie z echem w regule nan! = Nan.
Paul Panzer

3
C99 określa, że ​​rzeczywiste typy zmiennoprzecinkowe nie są promowane do złożonych, gdy są mnożone przez typ złożony (sekcja 6.3.1.8 projektu standardu) i, o ile wiem, to samo dotyczy std :: complex w C ++. Oznacza to, że 1 jest multiplikatywną tożsamością dla tych typów w tych językach. Python powinien zrobić to samo. Jego obecne zachowanie nazwałbym po prostu błędem.
benrg

2
@PaulPanzer: Nie, ale podstawową koncepcją byłoby to, że jedno zero (które nazywam Z) zawsze utrzymywało x + Z = x i x * Z = Z, i 1 / Z = NaN, jeden (dodatni nieskończenie mały) utrzymywałby 1 / P = + INF, jeden (ujemny nieskończenie mały) utrzymywałby 1 / N = -INF, a (nieskończenie mały bez znaku) dałby 1 / U = NaN. Ogólnie rzecz biorąc, xx byłoby U, chyba że x jest prawdziwą liczbą całkowitą, w którym to przypadku dałoby Z.
supercat

6

To jest szczegół implementacji, w jaki sposób złożone mnożenie jest zaimplementowane w CPythonie. W przeciwieństwie do innych języków (np. C lub C ++), CPython przyjmuje nieco uproszczone podejście:

  1. int / float są promowane do liczb zespolonych w mnożeniu
  2. stosowana jest prosta formuła szkolna , która nie zapewnia pożądanych / oczekiwanych rezultatów, gdy chodzi o nieskończone liczby:
Py_complex
_Py_c_prod(Py_complex a, Py_complex b)
{
    Py_complex r;
    r.real = a.real*b.real - a.imag*b.imag;
    r.imag = a.real*b.imag + a.imag*b.real;
    return r;
}

Jeden problematyczny przypadek z powyższym kodem to:

(0.0+1.0*j)*(inf+inf*j) = (0.0*inf-1*inf)+(0.0*inf+1.0*inf)j
                        =  nan + nan*j

Jednak chciałoby się mieć -inf + inf*jtaki wynik.

Pod tym względem inne języki nie są daleko do przodu: mnożenie liczb zespolonych przez długi czas nie było częścią standardu C, uwzględnione tylko w C99 jako dodatek G, który opisuje, jak należy wykonywać złożone mnożenie - i nie jest tak proste, jak powyższa formuła szkoły! Standard C ++ nie określa, jak złożone powinno działać mnożenie, dlatego większość implementacji kompilatorów powraca do implementacji C, która może być zgodna z C99 (gcc, clang) lub nie (MSVC).

W powyższym „problematycznym” przykładzie implementacje zgodne ze standardem C99 (które są bardziej skomplikowane niż formuła szkolna) dałyby ( zobacz na żywo ) oczekiwany wynik:

(0.0+1.0*j)*(inf+inf*j) = -inf + inf*j 

Nawet ze standardem C99 jednoznaczny wynik nie jest zdefiniowany dla wszystkich wejść i może być inny nawet dla wersji zgodnych z C99.

Innym efektem ubocznym floatbraku awansu complexw C99 jest to, że pomnożenie inf+0.0jz 1.0lub 1.0+0.0jmoże prowadzić do różnych wyników (patrz tutaj na żywo):

  • (inf+0.0j)*1.0 = inf+0.0j
  • (inf+0.0j)*(1.0+0.0j) = inf-nanj, część urojona będąca -nana nie nan(jak w przypadku CPythona) nie odgrywa tutaj roli, ponieważ wszystkie ciche nany są równoważne (zobacz to ), nawet niektóre z nich mają ustawiony bit znaku (i dlatego są drukowane jako "-", zobacz to ), a niektóre nie.

Co jest przynajmniej sprzeczne z intuicją.


Moim kluczowym wnioskiem jest to, że nie ma nic prostego w „prostym” mnożeniu (lub dzieleniu) liczb zespolonych i podczas przełączania się między językami, a nawet kompilatorami, należy przygotować się na subtelne błędy / różnice.


Wiem, że istnieje wiele nanobitowych wzorców. Nie wiedziałem jednak, co to za znak. Ale miałem na myśli semantycznie. Czym różni się -nan od nan? A może powinienem powiedzieć, że bardziej różni się niż nan jest od nan?
Paul Panzer,

@PaulPanzer To jest tylko szczegół implementacji tego, jak printfi podobne działa z double: patrzą na znak-bit, aby zdecydować, czy „-” ma zostać wydrukowane, czy nie (bez względu na to, czy jest to nan, czy nie). Więc masz rację, nie ma znaczącej różnicy między „nan” i „-nan”, co wkrótce naprawi tę część odpowiedzi.
ead

Ach, dobrze. Martwiłem się przez chwilę, że wszystko, o czym myślałem, że wiedziałem o FP, nie jest w rzeczywistości poprawne ...
Paul Panzer

Przepraszam, że denerwuję, ale czy jesteś pewien, że to „nie ma wyimaginowanego 1,0, tj. 1,0j, co nie jest tym samym, co 0,0 + 1,0j w odniesieniu do mnożenia”. jest poprawne? Ten załącznik G wydaje się określać typ czysto wyimaginowany (G.2), a także zalecać sposób jego mnożenia itp. (G.5.1)
Paul Panzer,

@PaulPanzer Nie, dziękuję za wskazanie problemów! Jako c ++ - programista, przeważnie widzę od C99-standard do C ++ - glases - wypadło mi z głowy, że C jest tu o krok do przodu - po raz kolejny masz rację.
ead

3

Zabawna definicja z Pythona. Jeśli mamy do rozwiązywania tego piórem i papierem Powiedziałbym, że oczekiwany wynik byłby expected: (inf + 0j)jak wskazał, ponieważ wiemy, że mamy na myśli normę 1tak (float('inf')+0j)*1 =should= ('inf'+0j):

Ale tak nie jest, jak widać ... po uruchomieniu otrzymujemy:

>>> Complex( float('inf') , 0j ) * 1
result: (inf + nanj)

Python rozumie to *1jako liczbę zespoloną, a nie normę, 1więc interpretuje jako, *(1+0j)a błąd pojawia się, gdy próbujemy zrobić inf * 0j = nanjtak , jak inf*0nie można go rozwiązać.

Co tak naprawdę chcesz zrobić (zakładając, że 1 to norma 1):

Przypomnijmy, że jeśli z = x + iyjest liczbą zespoloną z częścią rzeczywistą x i częścią urojoną y, sprzężona liczba zespolona zjest definiowana jako z* = x − iy, a wartość bezwzględna, zwana również norm of zjest definiowana jako:

wprowadź opis obrazu tutaj

Zakładając, że 1jest to norma 1, powinniśmy zrobić coś takiego:

>>> c_num = complex(float('inf'),0)
>>> value = 1
>>> realPart=(c_num.real)*value
>>> imagPart=(c_num.imag)*value
>>> complex(realPart,imagPart)
result: (inf+0j)

niezbyt intuicyjny, wiem ... ale czasami języki kodowania są definiowane w inny sposób niż to, czego używamy na co dzień.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.