Mengapa angka floating point tidak akurat?

198

Mengapa beberapa angka kehilangan akurasi ketika disimpan sebagai angka floating point?

Sebagai contoh, angka desimal 9.2dapat dinyatakan dengan tepat sebagai rasio dua bilangan bulat desimal ( 92/10), yang keduanya dapat dinyatakan secara tepat dalam biner ( 0b1011100/0b1010). Namun, rasio yang sama disimpan sebagai angka floating point tidak pernah sama persis dengan 9.2:

32-bit "single precision" float: 9.19999980926513671875
64-bit "double precision" float: 9.199999999999999289457264239899814128875732421875

Bagaimana angka yang tampaknya sederhana ini "terlalu besar" untuk diekspresikan dalam memori 64 bit ?

mhlester
sumber

Jawaban:

241

Dalam sebagian besar bahasa pemrograman, angka floating point diwakili banyak seperti notasi ilmiah : dengan eksponen dan mantra (juga disebut signifikansi). Angka yang sangat sederhana, katakanlah 9.2, sebenarnya adalah fraksi ini:

5179139571476070 * 2 -49

Di mana eksponen -49dan mantissa berada 5179139571476070. Alasan mengapa tidak mungkin untuk mewakili beberapa angka desimal dengan cara ini adalah bahwa eksponen dan mantissa harus bilangan bulat. Dengan kata lain, semua pelampung harus berupa bilangan bulat dikalikan dengan kekuatan bilangan bulat 2 .

9.2mungkin sederhana 92/10, tetapi 10 tidak dapat dinyatakan sebagai 2 n jika n terbatas pada nilai integer.


Melihat Data

Pertama, beberapa fungsi untuk melihat komponen yang menghasilkan 32- dan 64-bit float. Hapus ini jika Anda hanya peduli dengan output (contoh dalam Python):

def float_to_bin_parts(number, bits=64):
    if bits == 32:          # single precision
        int_pack      = 'I'
        float_pack    = 'f'
        exponent_bits = 8
        mantissa_bits = 23
        exponent_bias = 127
    elif bits == 64:        # double precision. all python floats are this
        int_pack      = 'Q'
        float_pack    = 'd'
        exponent_bits = 11
        mantissa_bits = 52
        exponent_bias = 1023
    else:
        raise ValueError, 'bits argument must be 32 or 64'
    bin_iter = iter(bin(struct.unpack(int_pack, struct.pack(float_pack, number))[0])[2:].rjust(bits, '0'))
    return [''.join(islice(bin_iter, x)) for x in (1, exponent_bits, mantissa_bits)]

Ada banyak kerumitan di balik fungsi itu, dan itu akan cukup jelas untuk dijelaskan, tetapi jika Anda tertarik, sumber daya penting untuk tujuan kita adalah modul struct .

Python float adalah angka 64-bit, presisi ganda. Dalam bahasa lain seperti C, C ++, Java dan C #, presisi ganda memiliki tipe terpisah double, yang sering diimplementasikan sebagai 64 bit.

Ketika kita memanggil fungsi itu dengan contoh kita 9.2, inilah yang kita dapatkan:

>>> float_to_bin_parts(9.2)
['0', '10000000010', '0010011001100110011001100110011001100110011001100110']

Menafsirkan Data

Anda akan melihat saya telah membagi nilai kembali menjadi tiga komponen. Komponen-komponen ini adalah:

  • Tanda
  • Eksponen
  • Mantissa (juga disebut Significand, atau Fraction)

Tanda

Tanda disimpan dalam komponen pertama sebagai bit tunggal. Sangat mudah dijelaskan: 0berarti pelampung adalah angka positif; 1berarti negatif. Karena 9.2positif, nilai tanda kami adalah0 .

Eksponen

Eksponen disimpan di komponen tengah sebagai 11 bit. Dalam kasus kami 0b10000000010,. Dalam desimal, itu mewakili nilai 1026. Keunikan dari komponen ini adalah Anda harus mengurangi angka sama dengan 2 (# bit) - 1 - 1 untuk mendapatkan eksponen yang benar; dalam kasus kami, itu berarti mengurangi 0b1111111111(angka desimal 1023) untuk mendapatkan eksponen yang sebenarnya, 0b00000000011(angka desimal 3).

Mantissa

Mantera disimpan dalam komponen ketiga sebagai 52 bit. Namun, ada kekhasan untuk komponen ini juga. Untuk memahami kekhasan ini, pertimbangkan nomor dalam notasi ilmiah, seperti ini:

6.0221413x10 23

Mantera akan menjadi 6.0221413. Ingatlah bahwa mantissa dalam notasi ilmiah selalu dimulai dengan satu digit bukan nol. Hal yang sama berlaku untuk biner, kecuali bahwa biner hanya memiliki dua digit: 0dan 1. Jadi mantissa biner selalu dimulai dengan 1! Ketika pelampung disimpan, 1bagian depan biner mantissa dihilangkan untuk menghemat ruang; kita harus meletakkannya kembali di depan elemen ketiga kita untuk mendapatkan mantissa yang sebenarnya :

1,0010011001100110011001100110011001100110011001100110

Ini melibatkan lebih dari sekedar tambahan sederhana, karena bit yang disimpan dalam komponen ketiga kami sebenarnya mewakili bagian fraksional mantissa, di sebelah kanan titik radix .

Ketika berhadapan dengan angka desimal, kita "memindahkan titik desimal" dengan mengalikan atau membagi dengan kekuatan 10. Dalam biner, kita dapat melakukan hal yang sama dengan mengalikan atau membagi dengan kekuatan 2. Karena elemen ketiga kita memiliki 52 bit, kita membagi dengan 2 52 untuk memindahkannya 52 tempat ke kanan:

0,0010011001100110011001100110011001100110011001100110

Dalam notasi desimal, itu sama dengan membagi 675539944105574oleh 4503599627370496untuk mendapatkan 0.1499999999999999. (Ini adalah salah satu contoh rasio yang dapat dinyatakan secara persis dalam biner, tetapi hanya kurang dalam desimal; untuk lebih jelasnya , lihat: 675539944105574/4503599627370496 .)

Sekarang kita telah mengubah komponen ketiga menjadi angka fraksional, menambahkan 1memberi mantissa yang sebenarnya.

Memasang Kembali Komponen

  • Tanda (komponen pertama): 0untuk positif, 1untuk negatif
  • Eksponen (komponen tengah): Kurangi 2 (# bit) - 1 - 1 untuk mendapatkan eksponen yang benar
  • Mantissa (komponen terakhir): Bagi dengan 2 (# bit) dan tambahkan 1untuk mendapatkan mantissa yang sebenarnya

Menghitung Angka

Menyatukan ketiga bagian ini, kami diberi nomor biner ini:

1,0010011001100110011001100110011001100110011001100110 x 10 11

Yang kemudian dapat kita konversi dari biner ke desimal:

1.1499999999999999 x 2 3 (tidak eksak!)

Dan kalikan untuk mengungkapkan representasi akhir dari jumlah kami mulai dengan ( 9.2) setelah disimpan sebagai nilai floating point:

9.1999999999999993


Mewakili sebagai Fraksi

9.2

Sekarang kita telah membangun nomornya, adalah mungkin untuk merekonstruksi menjadi pecahan sederhana:

1,0010011001100110011001100110011001100110011001100110 x 10 11

Pindahkan mantissa ke seluruh nomor:

10010011001100110011001100110011001100110011001100110 x 10 11-110100

Konversikan ke desimal:

5179139571476070 x 2 3-52

Kurangi eksponen:

5179139571476070 x 2 -49

Ubah eksponen negatif menjadi divisi:

5179139571476070/2 49

Gandakan eksponen:

5179139571476070/562949953421312

Yang sama dengan:

9.1999999999999993

9.5

>>> float_to_bin_parts(9.5)
['0', '10000000010', '0011000000000000000000000000000000000000000000000000']

Sudah Anda dapat melihat mantissa hanya 4 digit diikuti oleh banyak nol. Tapi mari kita pergi melalui langkah.

Merakit notasi ilmiah biner:

1,0011 x 10 11

Menggeser titik desimal:

10011 x 10 11-100

Kurangi eksponen:

10011 x 10 -1

Biner ke desimal:

19 x 2 -1

Eksponen negatif ke divisi:

19/2 1

Gandakan eksponen:

19/2

Sama dengan:

9.5



Bacaan lebih lanjut

mhlester
sumber
1
Ada juga tutorial yang bagus yang menunjukkan bagaimana cara sebaliknya - diberikan representasi desimal dari angka, bagaimana Anda membangun ekuivalen floating point. Pendekatan "pembagian panjang" menunjukkan dengan sangat jelas bagaimana Anda berakhir dengan "sisa" setelah mencoba untuk mewakili nomor tersebut. Harus ditambahkan jika Anda ingin benar-benar "kanonik" dengan jawaban Anda.
Floris
1
Jika Anda berbicara tentang Python dan floating-point, saya sarankan setidaknya menyertakan tutorial Python di tautan Anda: docs.python.org/3.4/tutorial/floatingpoint.html Itu seharusnya menjadi one-stop go-to sumber daya untuk masalah floating-point untuk programmer Python. Jika kurang dalam beberapa hal (dan hampir pasti), silakan buka masalah pada pelacak bug Python untuk pembaruan atau perubahan.
Mark Dickinson
@ mhlester Jika ini berubah menjadi wiki komunitas, silakan sertakan jawaban saya ke dalam wiki Anda.
Nicu Stiurca
5
Jawaban ini tentunya juga harus terhubung ke floating-point-gui.de , karena ini mungkin pengantar terbaik untuk pemula. IMO, itu bahkan harus pergi di atas "Apa yang setiap ilmuwan komputer harus tahu ..." - hari ini, orang-orang yang dapat memahami kertas Goldberg biasanya sudah sangat menyadarinya.
Daniel Pryden
1
"Ini adalah salah satu contoh rasio yang dapat diekspresikan tepat dalam biner, tetapi hanya kira-kira dalam desimal". Ini tidak benar. Semua angka 'di atas pangkat dua' ini tepat dalam desimal. Setiap perkiraan hanya untuk mempersingkat angka desimal - untuk kenyamanan.
Rick Regan
29

Ini bukan jawaban lengkap ( mhlester sudah membahas banyak hal baik yang tidak akan saya tiru), tetapi saya ingin menekankan seberapa besar representasi angka tergantung pada basis tempat Anda bekerja.

Pertimbangkan fraksi 2/3

Dalam basis 10 yang bagus, kami biasanya menuliskannya sebagai sesuatu seperti

  • 0,666 ...
  • 0,666
  • 0,667

Ketika kita melihat representasi itu, kita cenderung mengasosiasikan masing-masing dengan fraksi 2/3, meskipun hanya representasi pertama secara matematis sama dengan fraksi. Representasi / perkiraan kedua dan ketiga memiliki kesalahan pada urutan 0,001, yang sebenarnya jauh lebih buruk daripada kesalahan antara 9,2 dan 9,1999999999999993. Bahkan, representasi kedua bahkan tidak dibulatkan dengan benar! Namun demikian, kami tidak memiliki masalah dengan 0,666 sebagai perkiraan dari angka 2/3, jadi kami seharusnya tidak benar-benar memiliki masalah dengan bagaimana 9,2 diperkirakan di sebagian besar program . (Ya, dalam beberapa program itu penting.)

Basis nomor

Jadi di sinilah basis bilangan sangat penting. Jika kami mencoba mewakili 2/3 di basis 3, maka

(2/3) 10 = 0,2 3

Dengan kata lain, kami memiliki representasi yang tepat dan terbatas untuk nomor yang sama dengan mengganti basis! Kesimpulannya adalah bahwa meskipun Anda dapat mengonversi angka apa pun ke pangkalan apa pun, semua bilangan rasional memiliki representasi terbatas yang tepat di beberapa pangkalan tetapi tidak di yang lain .

Untuk mengantar titik ini pulang, mari kita lihat 1/2. Mungkin mengejutkan Anda bahwa meskipun angka yang sangat sederhana ini memiliki representasi yang tepat di basis 10 dan 2, ia membutuhkan representasi berulang di basis 3.

(1/2) 10 = 0,5 10 = 0,1 2 = 0,1111 ... 3

Mengapa angka floating point tidak akurat?

Karena sering kali, mereka mendekati rasional yang tidak dapat diwakili secara terbatas dalam basis 2 (angka-angka berulang), dan secara umum mereka mendekati angka nyata (mungkin tidak rasional) yang mungkin tidak dapat diwakili dalam banyak digit dalam basis apa pun .

Nicu Stiurca
sumber
3
Jadi dengan kata lain, base-3 akan sempurna untuk 1/3seperti base-10 sempurna untuk 1/10. Fraksi tidak bekerja di basis-2
mhlester
2
@ mhlester Ya. Dan secara umum, basis-N sempurna untuk setiap pecahan yang penyebutnya Natau kelipatannya.
Nicu Stiurca
2
Dan ini adalah salah satu alasan mengapa beberapa kotak alat numerik melacak "apa yang dibagi dengan apa", dan dalam prosesnya dapat menjaga "akurasi tak terbatas" untuk semua bilangan rasional. Sama seperti fisikawan yang ingin menjaga persamaan mereka simbolis sampai saat-saat terakhir yang mungkin, jika faktor πdll dibatalkan.
Floris
3
@ Floris Saya juga telah melihat kasus di mana algoritma yang hanya melakukan aritmatika dasar (yaitu, mempertahankan rasionalitas input), menentukan apakah inputnya (kemungkinan) rasional, melakukan matematika menggunakan aritmatika floating point normal, kemudian memperkirakan kembali rasional aproksimasi pada akhirnya untuk memperbaiki kesalahan pembulatan. Secara khusus, algoritma bentuk eselon baris tereduksi Matlab melakukan ini, dan ini sangat membantu stabilitas numerik.
Nicu Stiurca
@SchighSchagh - menarik, saya tidak tahu itu. Saya tahu bahwa stabilitas numerik adalah sesuatu yang tidak diajarkan secara memadai pada zaman ini dengan ketepatan ganda ganda. Yang berarti bahwa banyak yang kehilangan pembelajaran tentang keanggunan banyak algoritma yang indah. Saya sangat suka algoritma yang menghitung dan memperbaiki kesalahan mereka sendiri.
Floris
13

Meskipun semua jawaban lain baik, masih ada satu hal yang hilang:

Tidak mungkin untuk mewakili bilangan irasional (misalnya π sqrt(2),,log(3) , dll) tepatnya!

Dan itulah mengapa mereka disebut irasional. Tidak ada jumlah penyimpanan bit di dunia akan cukup untuk menampung bahkan satu dari mereka. Hanya simbolis aritmatika yang mampu mempertahankan presisi mereka.

Meskipun jika Anda akan membatasi kebutuhan matematika Anda ke bilangan rasional hanya masalah ketelitian menjadi dapat dikelola. Anda perlu menyimpan sepasang bilangan bulat (mungkin sangat besar) adan bmenahan angka yang diwakili oleh fraksi a/b. Semua aritmatika Anda harus dilakukan pada pecahan seperti dalam matematika sekolah menengah (misalnya a/b * c/d = ac/bd).

Tapi tentu saja Anda masih akan mengalami yang sama kesulitan ketika pi, sqrt, log, sin, dll yang terlibat.

TL; DR

Untuk aritmatika yang dipercepat perangkat keras, hanya sejumlah angka rasional yang dapat direpresentasikan. Setiap angka yang tidak dapat diwakili diperkirakan. Beberapa angka (yaitu irasional) tidak pernah dapat direpresentasikan tidak peduli sistemnya.

Benjolan
sumber
4
Menariknya, basis irasional memang ada. Phinary , misalnya.
Veedrac
5
bilangan irasional dapat (hanya) diwakili dalam basisnya. Sebagai contoh pi adalah 10 dalam basis pi
phuclv
4
Poin tetap valid: Beberapa angka tidak pernah dapat diwakili, apa pun sistemnya. Anda tidak mendapatkan apa-apa dengan mengubah basis Anda karena beberapa nomor lainnya tidak dapat diwakili lagi.
LumpN
4

Ada bilangan real yang tak terhingga banyaknya (begitu banyak sehingga Anda tidak dapat menghitungnya), dan ada banyak bilangan rasional tak terhingga (dimungkinkan untuk menghitungnya).

Representasi floating-point adalah yang terbatas (seperti apa pun di komputer) sehingga banyak sekali banyak angka yang tidak mungkin untuk diwakili. Secara khusus, 64 bit hanya memungkinkan Anda untuk membedakan hanya 18.446.744.073.709.551.616 nilai yang berbeda (yang tidak seberapa dibandingkan dengan tak terbatas). Dengan konvensi standar, 9.2 bukan salah satunya. Yang bisa dari bentuk m.2 ^ e untuk beberapa bilangan bulat m dan e.


Anda mungkin datang dengan sistem penomoran yang berbeda, 10 berbasis misalnya, di mana 9,2 akan memiliki representasi yang tepat. Tetapi angka lainnya, katakanlah 1/3, masih tidak mungkin untuk diwakili.


Juga perhatikan bahwa angka floating-point presisi ganda sangat akurat. Mereka dapat mewakili angka apa pun dalam rentang yang sangat luas dengan sebanyak 15 digit tepat. Untuk perhitungan kehidupan sehari-hari, 4 atau 5 digit lebih dari cukup. Anda tidak akan pernah benar-benar membutuhkan 15 itu, kecuali jika Anda ingin menghitung setiap milidetik dalam hidup Anda.

Yves Daoust
sumber
1

Mengapa kita tidak bisa mewakili 9.2 dalam floating point biner?

Nomor titik mengambang adalah (menyederhanakan sedikit) sistem penomoran posisi dengan jumlah digit terbatas dan titik radix bergerak.

Fraksi hanya dapat diekspresikan dengan tepat menggunakan jumlah digit terbatas dalam sistem penomoran posisi jika faktor utama penyebut (ketika fraksi dinyatakan dalam istilah terendah) adalah faktor basis.

Faktor prima dari 10 adalah 5 dan 2, jadi pada basis 10 kita dapat mewakili sebagian kecil dari bentuk a / (2 b 5 c ).

Di sisi lain satu-satunya faktor prima dari 2 adalah 2, jadi pada basis 2 kita hanya dapat mewakili fraksi dari bentuk a / (2 b )

Mengapa komputer menggunakan representasi ini?

Karena ini adalah format sederhana untuk dikerjakan dan cukup akurat untuk sebagian besar tujuan. Pada dasarnya alasan yang sama para ilmuwan menggunakan "notasi ilmiah" dan membulatkan hasil mereka ke jumlah digit yang wajar pada setiap langkah.

Tentunya dimungkinkan untuk menentukan format fraksi, dengan (misalnya) pembilang 32-bit dan penyebut 32-bit. Itu akan dapat mewakili angka-angka yang tidak dapat diapung oleh floating point presisi IEEE, tetapi juga akan ada banyak angka yang dapat diwakili dalam floating point presisi ganda yang tidak dapat diwakili dalam format fraksi ukuran tetap.

Namun masalah besar adalah format seperti itu sulit dilakukan perhitungan. Karena dua alasan.

  1. Jika Anda ingin memiliki tepat satu representasi dari masing-masing angka maka setelah setiap perhitungan Anda perlu mengurangi pecahan menjadi istilah terendah. Itu berarti bahwa untuk setiap operasi pada dasarnya Anda perlu melakukan perhitungan pembagi umum terbesar.
  2. Jika setelah perhitungan Anda berakhir dengan hasil yang tidak dapat direpresentasikan karena pembilang atau penyebut Anda perlu menemukan hasil yang terdekat yang dapat diwakili. Ini bukan trivil.

Beberapa Bahasa memang menawarkan jenis fraksi, tetapi biasanya mereka melakukannya dalam kombinasi dengan presisi arbiter, ini menghindari perlu khawatir tentang mendekati fraksi tetapi itu menciptakan masalah sendiri, ketika angka melewati sejumlah besar perhitungan langkah ukuran penyebut dan maka penyimpanan yang dibutuhkan untuk fraksi dapat meledak.

Beberapa bahasa juga menawarkan tipe floating point desimal, ini terutama digunakan dalam skenario di mana penting bahwa hasil komputer sesuai dengan aturan pembulatan yang sudah ada sebelumnya yang ditulis dengan manusia dalam pikiran (terutama perhitungan keuangan). Ini sedikit lebih sulit untuk dikerjakan daripada binary floating point, tetapi masalah terbesarnya adalah kebanyakan komputer tidak menawarkan dukungan perangkat keras untuk mereka.

plugwash
sumber
-4

Coba ini

DecimalFormat decimalFormat = new DecimalFormat("#.##");
String.valueOf(decimalFormat.format(decimalValue))));

' decimalValue' adalah nilai Anda untuk dikonversi.

Popal
sumber