15. Liukulukujen aritmetiikka: Issues and Limitations¶

Liukulukuluvut esitetään tietokonelaitteistossa perus-2 (binäärisinä)murtolukuina. Esimerkiksi desimaaliosuudella

0.125

on arvo 1/10 + 2/100 + 5/1000, ja samalla tavalla binääriosuudella

0.001

on arvo 0/2 + 0/4 + 1/8. Nämä kaksi murtolukua ovat identtiset arvot, ainoa todellinen ero on, että ensimmäinen on kirjoitettu base 10 murto notaatio, ja toinen base 2.

valitettavasti useimpia desimaalilukuja ei voida esittää täsmälleen samalla tavalla kuin binaryfraktioita. Tästä seuraa, että syöttämiäsi desimaalilukuja approksimoidaan yleensä vain koneeseen tallennettujen binääristen liukulukulukujen avulla.

ongelma on aluksi helpompi ymmärtää tukikohdassa 10. Mieti fraktio1 / 3. Voit arvioida, että base 10 murto:

0.3

tai, parempi,

0.33

tai, parempi,

0.333

ja niin edelleen. Ei ole väliä kuinka monta numeroa olet valmis kirjoittamaan, tulos ei koskaan ole täsmälleen 1/3, mutta on yhä parempi likiarvo 1 / 3.

samoin, ei väliä kuinka monta perus-2-merkkiä haluat käyttää, thedekimaaliarvoa 0.1 ei voida esittää täsmälleen perus-2-murtoluvuksi. Base2: ssa 1/10 on äärettömän toistuva murtoluku

0.0001100110011001100110011001100110011001100110011...

pysähdy mihin tahansa äärelliseen bittimäärään, ja saat likiarvon. Useimmilla koneilla nykyään kellukkeet approksimoidaan käyttäen binääristä murtolukua osoittajan avulla käyttäen 53 ensimmäistä bittiä alkaen merkittävimmästä bittistä ja nimittäjän ollessa kahden potenssi. Kun kyseessä on 1/10, binäärinen fraktiointi on3602879701896397 / 2 ** 55, joka on lähellä 1/10: n todellista arvoa, mutta ei täsmälleen samanarvoinen.

monet käyttäjät eivät ole tietoisia likiarvosta, koska arvot on displayed. Python tulostaa vain desimaalin approksimaation koneen tallentaman binäärisen approksimaation todelliseen desimaaliarvoon. Useimmissa koneissa, ifPython oli tulostaa todellinen desimaaliarvo binary approksimaatio storedfor 0.1, sen täytyisi näyttää

>>> 0.10.1000000000000000055511151231257827021181583404541015625

se on enemmän numeroita kuin useimmat pitävät hyödyllisinä, joten Python pitää numeroiden lukumäärän hallittavana näyttämällä sen sijaan pyöristettyä arvoa

>>> 1 / 100.1

muista vain, että vaikka tulostettu tulos näyttää 1/10: n tarkalta arvolta, todellinen tallennettu arvo on lähin edustettavissa oleva binääriosuus.

mielenkiintoista on, että on olemassa monia eri desimaalilukuja, joilla on sama likimääräinen binääriluku. Esimerkiksi numerot 0.1 ja0.10000000000000001 ja0.1000000000000000055511151231257827021181583404541015625 ovat allapropproksimoituja 3602879701896397 / 2 ** 55. Koska kaikilla näillä desimaaliarvoilla on sama approksimaatio, mikä tahansa niistä voidaan displayed, säilyttäen kuitenkin invariantti eval(repr(x)) == x.

historiallisesti Pythonin kehote ja sisäänrakennettu repr() funktio valitsisi yhden, jolla on 17 merkitsevää numeroa, 0.10000000000000001. Alkaen Python 3: sta.1, Python (useimmissa järjestelmissä) pystyy nyt valitsemaan näistä lyhimmän ja näyttää yksinkertaisesti 0.1.

huomaa, että tämä on binäärisen liukupisteen luonne: tämä ei ole bugi Python, eikä se ole bugi koodissasikaan. Näet samantyyppisiä asioita kaikissa kielissä, jotka tukevat laitteistosi liukulukuaritmetiikkaa (vaikka jotkin kielet eivät välttämättä näytä eroa oletusarvoisesti tai alloutput-tilassa).

miellyttävämpää tulostusta varten saatat haluta käyttää merkkijonomuotoilua tuottamaan rajoitetun määrän merkitseviä numeroita:

>>> format(math.pi, '.12g') # give 12 significant digits'3.14159265359'>>> format(math.pi, '.2f') # give 2 digits after the point'3.14'>>> repr(math.pi)'3.141592653589793'

on tärkeää ymmärtää, että tämä on todellisessa mielessä illuusio: pyöristät yksinkertaisesti todellisen konearvon näytön.

yksi illuusio voi synnyttää toisen. Esimerkiksi, koska 0,1 ei ole täsmälleen 1/10, kolmen arvon summaaminen 0, 1: stä ei välttämättä saa täsmälleen 0, 3: a:

>>> .1 + .1 + .1 == .3False

myös, koska 0, 1 ei pääse yhtään lähemmäksi tarkkaa arvoa 1/10 ja 0.3 ei pääse yhtään lähemmäksi tarkkaa arvoa 3/10, sitten pyöristetään ennaltaround() funktio ei voi auttaa:

>>> round(.1, 1) + round(.1, 1) + round(.1, 1) == round(.3, 1)False

vaikka lukuja ei voida tehdä lähemmäksi aiottuja tarkkoja arvoja,round() funktio voi olla hyödyllinen jälkikääntämisessä siten, että epätarkat arvot tulevat keskenään vertailukelpoisiksi:

>>> round(.1 + .1 + .1, 10) == round(.3, 10)True

binäärinen liukulukuaritmetiikka pitää sisällään monia tämänkaltaisia yllätyksiä. Problemwith ” 0.1 ”on selitetty tarkasti alla,”edustus virhe” osiossa. Katso leijuvan pisteen vaarat kattavammasta kertomuksesta muista yleisistä yllätyksistä.

kuten lopussa sanotaan, ”helppoja vastauksia ei ole.”Älkää silti olko epäröimättä kelluva piste! Python float-toimintojen virheet periytyvät liukulukulaitteistosta, ja useimmissa koneissa ne ovat nomoren luokkaa kuin 1 osa 2**53: sta per toiminto. Se on enemmän kuin riittävä mosttasks, mutta sinun täytyy pitää mielessä, että se ei ole desimaali aritmeettinen ja että jokainen float toiminta voi kärsiä uuden pyöristysvirhe.

vaikka patologisia tapauksia on olemassa, useimmissa satunnaisissa kelluntapisteritmeettisissä tapauksissa näet lopulta odottamasi tuloksen, jos yksinkertaisesti pyöräytät lopullisten tuloksiesi määrän odotettuun desimaalilukuun.str() yleensä riittää, ja tarkemmasta ohjauksesta katso str.format()metodin muotomäärittelyt Formaattijonosyntaksissa.

käyttötapauksissa, jotka vaativat tarkan desimaaliesityksen, kokeiledecimal modulia, joka toteuttaa desimaaliaritmetiikan, joka soveltuu laskentasovelluksiin ja korkean tarkkuuden sovelluksiin.

toista tarkan aritmetiikan muotoa tukee fractions Module, joka toteuttaa rationaalilukuihin perustuvan aritmetiikan (eli luvut kuten 1 / 3 voidaan esittää täsmälleen).

Jos käytät paljon liukulukuoperaatioita, kannattaa tutustua numeeriseen Python-pakettiin ja moniin muihin scipy-projektin toimittamiin matemaattisiin ja tilastollisiin operaatioihin. <https://scipy.org>.

Python tarjoaa työkaluja, joista voi olla apua niissäkin harvoissa tapauksissa, joissa haluat todella tietää kellunnan tarkan arvon. float.as_integer_ratio() menetelmä ilmaisee kellunnan arvon afraktiona:

>>> x = 3.14159>>> x.as_integer_ratio()(3537115888337719, 1125899906842624)

koska suhde on tarkka, voidaan sen avulla luoda häviöttömästi alkuperäinen arvo:

>>> x == 3537115888337719 / 1125899906842624True

Thefloat.hex()menetelmä ilmaisee kellunnan heksadesimaalisena (base16), jolloin saadaan jälleen tietokoneen tallentama tarkka arvo:

>>> x.hex()'0x1.921f9f01b866ep+1'

tätä tarkkaa heksadesimaaliesitystä voidaan käyttää rekonstruoimaan kellutusarvo täsmälleen:

>>> x == float.fromhex('0x1.921f9f01b866ep+1')True

koska esitysmuoto on tarkka, siitä on hyötyä arvojen luotettavasti siirtämisessä Pythonin eri versioiden välillä (platform independence) ja tietojen vaihtamisessa muiden samaa muotoa tukevien kielten kanssa (kuten Java ja C99).

toinen hyödyllinen työkalu on math.fsum() funktio, joka auttaa mitigateloss-of-precision summauksen aikana. Se seuraa ”Kadonneet numerot” arvot lisätään päälle käynnissä yhteensä. Tämä voi vaikuttaa kokonaistarkkuuteen siten, että virheet eivät kasaudu siihen pisteeseen, jossa ne vaikuttavat loppusummaan:

>>> sum( * 10) == 1.0False>>> math.fsum( * 10) == 1.0True

15.1. Esitysvirhe¶

tässä osiossa selitetään ” 0.1 ” – esimerkki yksityiskohtaisesti ja näytetään, miten voit suorittaa tämänkaltaisten tapausten tarkan analyysin itse. Perus perehtyneisyys binaryfloating-point representaatio oletetaan.

Edustusvirhe viittaa siihen, että joitakin (useimpia, itse asiassa)desimaalilukuja ei voida esittää täsmälleen samalla tavalla kuin binäärisiä (kantaluku 2) murtolukuja.Tämä on pääsyy siihen, miksi Python (tai Perl, C, C++, Java, Fortran ja monet muut) ei usein näytä tarkkaa desimaalilukua, jota odotat.

miksi? 1/10 ei ole aivan edustettavissa binäärisenä murtolukuna. Lähes allmachines tänään (marraskuu 2000) käyttää IEEE-754 liukuluku aritmeettinen, ja lähes kaikki alustat kartta Python kelluu IEEE-754 ”kaksinkertainen tarkkuus”. 754doubles sisältää 53 bittiä tarkkuutta, joten syötettäessä tietokone pyrkii muuntamaan 0,1 lähimpään murto-osaan, jonka se voi muodossa J / 2**N, jossa J isan kokonaisluku sisältää täsmälleen 53 bittiä. Uudelleenkirjoittaminen

1 / 10 ~= J / (2**N)

as

J ~= 2**N / 10

ja muistuttaa,että J: llä on tasan 53 bittiä (is>= 2**52mutta < 2**53), paras arvo N: lle on 56:

>>> 2**52 <= 2**56 // 10 < 2**53True

eli 56 on ainoa arvo N: lle, joka jättää j: lle tasan 53 bittiä. Paras mahdollinen arvo J on sitten, että osamäärä pyöristetty:

>>> q, r = divmod(2**56, 10)>>> r6

koska jäljellä on yli puolet 10: stä, saadaan paras likiarvo pyöristämällä ylös:

>>> q+17205759403792794

näin ollen paras mahdollinen likiarvo 1/10 in 754 kaksinkertainen tarkkuus on:

7205759403792794 / 2 ** 56

jakamalla sekä Osoittaja että nimittäjä kahdella vähennetään murtoluku:

3602879701896397 / 2 ** 55

huomaa, että koska pyöristimme ylös, tämä on itse asiassa hieman suurempi kuin 1/10;jos emme olisi pyöristäneet ylöspäin, osamäärä olisi ollut hieman pienempi kuin 1/10. Mutta missään tapauksessa se ei voi olla täsmälleen 1/10!

joten tietokone ei koskaan ”näe” 1/10: se mitä se näkee on tarkka murtoluku givenabove, paras 754 kaksinkertainen likiarvo, jonka se voi saada:

>>> 0.1 * 2 ** 553602879701896397.0

Jos kerromme tuon murtoluvun 10**55: llä, voimme nähdä arvon 55 desimaalilukuun:

>>> 3602879701896397 * 10 ** 55 // 2 ** 551000000000000000055511151231257827021181583404541015625

eli tietokoneeseen tallennettu tarkka luku on yhtä suuri kuin desimaaliarvo 0.1000000000000000055511151231257827021181583404541015625.Täyden desimaaliarvon näyttämisen sijaan monet kielet (mm. Pythonin myöhemmät versiot) pyöräyttävät tuloksen 17 merkitsevään numeroon:

>>> format(0.1, '.17f')'0.10000000000000001'

fractions ja decimal moduulit tekevät näistä calculationseasy:

>>> from decimal import Decimal>>> from fractions import Fraction>>> Fraction.from_float(0.1)Fraction(3602879701896397, 36028797018963968)>>> (0.1).as_integer_ratio()(3602879701896397, 36028797018963968)>>> Decimal.from_float(0.1)Decimal('0.1000000000000000055511151231257827021181583404541015625')>>> format(Decimal.from_float(0.1), '.17')'0.10000000000000001'

Vastaa

Sähköpostiosoitettasi ei julkaista. Pakolliset kentät on merkitty *