15. Aritmatika Pecahan Floating Point: Masalah dan Keterbatasan

Angka pecahan floating point diwakili dalam perangkat keras komputer sebagai pecahan basis 2 (biner). Misalnya, pecahan desimal:

0.125

memiliki nilai 1/10 + 2/100 + 5/1000, dan dengan cara yang sama pecahan biner

0.001

memiliki nilai 0/2 + 0/4 + 1/8. Dua pecahan ini memiliki nilai yang identik, satu-satunya perbedaan nyata adalah bahwa yang pertama ditulis dalam notasi fraksi basis 10, dan yang kedua dalam basis 2.

Sayangnya, sebagian besar pecahan desimal tidak dapat direpresentasikan persis dengan pecahan biner. Konsekuensinya adalah bahwa, secara umum, angka pecahan floating-point desimal yang Anda masukkan hanya didekati oleh angka-angka pecahan floating-point biner yang sebenarnya disimpan dalam mesin.

Masalahnya lebih mudah dipahami pada awalnya di basis 10. Pertimbangkan fraksi 1/3. Anda dapat memperkirakannya sebagai pecahan basis 10:

0.3

atau, lebih baik,

0.33

atau, lebih baik,

0.333

dan seterusnya. Tidak peduli berapa banyak digit yang Anda ingin tulis, hasilnya tidak akan pernah benar-benar 1/3, tetapi akan menjadi perkiraan yang semakin baik dari 1/3.

Dengan cara yang sama, tidak peduli berapa banyak digit basis 2 yang ingin Anda gunakan, nilai desimal 0.1 tidak dapat direpresentasikan persis sebagai fraksi basis 2. Dalam basis 2, 1/10 adalah percahan berulang yang tak terhingga

0.0001100110011001100110011001100110011001100110011...

Berhenti pada jumlah bit yang terbatas, dan Anda mendapatkan perkiraan. Pada kebanyakan mesin saat ini, float diperkirakan menggunakan percahan biner dengan pembilang menggunakan 53 bit pertama dimulai dengan bit paling signifikan dan dengan penyebut sebagai pangkat dua. Dalam kasus 1/10, fraksi biner adalah 3602879701896397 / 2 ** 55 yang dekat dengan tetapi tidak persis sama dengan nilai sebenarnya dari 1/10.

Banyak pengguna tidak menyadari pendekatan tentang bagaimana cara nilai ditampilkan. Python hanya mencetak perkiraan desimal ke nilai desimal sebenarnya dari perkiraan biner yang disimpan oleh mesin. Pada kebanyakan mesin, jika Python mencetak nilai desimal sebenarnya dari perkiraan biner yang disimpan untuk 0.1, ia harus menampilkan

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

Itu lebih banyak angka daripada yang dianggap berguna oleh kebanyakan orang, jadi Python menjaga jumlah angka tetap dapat dikelola dengan menampilkan nilai bulat sebagai gantinya

>>> 1 / 10
0.1

Hanya ingat, meskipun hasil cetakannya terlihat seperti nilai tepat 1/10, nilai sebenarnya yang disimpan adalah pecahan biner terdekat yang dapat direpresentasikan.

Menariknya, ada banyak angka desimal berbeda yang memiliki pecahan biner perkiraan terdekat yang sama. Misalnya, angka 0.1 dan 0.10000000000000001 dan 0.1000000000000000055511151231257827021181583404541015625 semuanya didekati oleh 3602879701896397 / 2 ** 55. Karena semua nilai desimal ini memiliki perkiraan yang sama, salah satu dari nilai tersebut dapat ditampilkan sambil tetap mempertahankan invarian lainnya eval(repr(x)) == x.

Secara historis, Python prompt dan fungsi bawaan repr() akan memilih satu dengan 17 digit signifikan, 0.10000000000000001. Dimulai dengan Python 3.1, Python (pada kebanyakan sistem) sekarang dapat memilih yang paling pendek dan hanya menampilkan 0.1.

Perhatikan bahwa ini adalah sifat dasar dari pecahan floating-point biner: ini bukan bug di Python, dan ini juga bukan bug dalam kode Anda. Anda akan melihat hal yang sama dalam semua bahasa yang mendukung aritmatika pecahan floating-point perangkat keras Anda (meskipun beberapa bahasa mungkin tidak display perbedaan secara default, atau dalam semua mode keluaran).

Untuk hasil yang lebih menyenangkan, Anda mungkin ingin menggunakan pemformatan string untuk menghasilkan jumlah digit signifikan yang terbatas:

>>> 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'

Sangat penting untuk menyadari bahwa ini adalah, dalam arti sebenarnya, sebuah ilusi: Anda hanya membulatkan display dari nilai mesin yang sebenarnya.

Satu ilusi mungkin melahirkan yang lain. Misalnya, karena 0.1 tidak tepat 1/10, menjumlahkan tiga nilai 0.1 mungkin tidak menghasilkan tepat 0.3, baik:

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

Juga, karena 0.1 tidak bisa mendekati nilai tepat 1/10 dan 0.3 tidak bisa mendekati nilai tepat 3/10, maka pra-pembulatan dengan fungsi round() tidak dapat membantu:

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

Meskipun angka tidak dapat dibuat lebih dekat dengan nilai pastinya, fungsi round() dapat berguna untuk post-rounding sehingga hasil dengan nilai yang tidak tepat menjadi sebanding satu sama lain:

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

Aritmatika pecahan floating-point biner memiliki banyak kejutan seperti ini. Masalah dengan "0.1" dijelaskan secara rinci di bawah ini, di bagian "Representation Error". Lihat Perils of Floating Point untuk penjelasan lebih lengkap tentang kejutan umum lainnya.

Seperti yang dikatakan menjelang akhir, "tidak ada jawaban yang mudah." Namun, jangan terlalu waspada terhadap pecahan floating point! Kesalahan dalam operasi float Python diwarisi dari pecahan floating point perangkat keras, dan pada kebanyakan mesin ada di urutan tidak lebih dari 1 bagian dalam 2**53 per operasi. Itu lebih dari cukup untuk sebagian besar tugas, tetapi Anda perlu ingat bahwa itu bukan aritmatika desimal dan bahwa setiap operasi float dapat mengalami kesalahan pembulatan baru.

Sementara kasus patologis memang ada, untuk sebagian besar penggunaan aritmatika floating-point yang santai Anda akan melihat hasil yang Anda harapkan pada akhirnya jika Anda hanya membulatkan tampilan hasil akhir Anda ke jumlah angka desimal yang Anda harapkan. str() biasanya mencukupi, dan untuk kontrol yang lebih baik lihat format str.format() penentu format di Format String Syntax.

Untuk kasus penggunaan yang memerlukan representasi desimal yang tepat, coba gunakan modul decimal yang mengimplementasikan aritmatika desimal yang cocok untuk aplikasi akuntansi dan aplikasi presisi tinggi.

Bentuk lain dari aritmatika yang tepat didukung oleh modul fractions yang mengimplementasikan aritmatika berdasarkan bilangan rasional (sehingga angka seperti 1/3 dapat direpresentasikan secara tepat).

Jika Anda adalah pengguna berat operasi floating point, Anda harus melihat pada paket Numerical Python dan banyak paket lainnya untuk operasi matematika dan statistik yang disediakan oleh proyek SciPy. Lihat <https://scipy.org>.

Python menyediakan alat yang dapat membantu pada saat-saat langka ketika Anda benar-benar do ingin tahu nilai pasti float. Metode float.as_integer_ratio() menyatakan nilai float sebagai pecahan:

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

Karena rasio ini tepat, dapat digunakan untuk membuat ulang nilai asli tanpa berkurang lossless:

>>> x == 3537115888337719 / 1125899906842624
True

Metode float.hex() mengekspresikan float dalam heksadesimal (basis 16), sekali lagi memberikan nilai tepat yang disimpan oleh komputer Anda:

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

Representasi heksadesimal yang tepat ini dapat digunakan untuk merekonstruksi nilai float dengan tepat:

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

Karena representasinya tepat, maka berguna untuk porting nilai secara andal di berbagai versi Python (platform independensi) dan pertukaran data dengan bahasa lain yang mendukung format yang sama (seperti Java dan C99).

Alat lain yang bermanfaat adalah fungsi math.fsum() yang membantu mengurangi kehilangan presisi selama penjumlahan. Ini melacak "lost digits" karena nilai ditambahkan ke total yang sedang berlangsung. Itu dapat membuat perbedaan dalam akurasi keseluruhan sehingga kesalahan tidak terakumulasi ke titik di mana mereka mempengaruhi total akhir:

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

15.1. Kesalahan Representasi

Bagian ini menjelaskan contoh "0.1" secara terperinci, dan menunjukkan bagaimana Anda dapat melakukan analisis yang tepat atas kasus-kasus seperti ini sendiri. Diasumsikan terbiasa secara mendasar dengan representasi pecahan floating point biner.

Representation error mengacu pada fakta bahwa beberapa pecahan desimal (sebagian besar, sebenarnya) tidak dapat direpresentasikan persis sebagai pecahan biner (basis 2). Ini adalah alasan utama mengapa Python (atau Perl, C, C++, Java, Fortran, dan banyak lainnya) sering tidak akan menampilkan angka desimal tepat yang Anda harapkan.

Mengapa demikian? 1/10 tidak tepat direpresentasikan sebagai pecahan biner. Hampir semua mesin saat ini (November 2000) menggunakan aritmetika pecahan floating point IEEE-754, dan hampir semua platform memetakan float Python ke IEEE-754 "double precision". 754 double mengandung 53 bit presisi, sehingga pada input komputer berusaha untuk mengkonversi 0.1 ke fraksi terdekat dari bentuk J/2***N* di mana J adalah bilangan bulat yang mengandung persis 53 bit. Menulis ulang

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

sebagai

J ~= 2**N / 10

dan mengingat bahwa J memiliki tepat 53 bit (adalah >= 2**52 tetapi < 2**53), nilai terbaik untuk N adalah 56:

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

Artinya, 56 adalah satu-satunya nilai untuk N yang meninggalkan J dengan tepat 53 bit. Nilai terbaik untuk J adalah bahwa hasil bagi dibulatkan:

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

Karena sisanya lebih dari setengah dari 10, perkiraan terbaik diperoleh dengan membulatkan ke atas:

>>> q+1
7205759403792794

Oleh karena itu perkiraan terbaik untuk 1/10 dalam 754 presisi double adalah

7205759403792794 / 2 ** 56

Membagi pembilang dan penyebut dengan dua mengurangi pecahan menjadi:

3602879701896397 / 2 ** 55

Perhatikan bahwa sejak kami mengumpulkan, ini sebenarnya sedikit lebih besar dari 1/10; jika kita belum mengumpulkan, hasil bagi akan sedikit lebih kecil dari 1/10. Tetapi tidak dapatkah hal itu exactly 1/10!

Jadi komputer tidak pernah "sees" 1/10: apa yang dilihatnya adalah pecahan tepat yang diberikan di atas, perkiraan 754 double terbaik yang bisa didapatnya:

>>> 0.1 * 2 ** 55
3602879701896397.0

Jika kita mengalikan pecahan itu dengan 10**55, kita bisa melihat nilainya menjadi 55 angka desimal:

>>> 3602879701896397 * 10 ** 55 // 2 ** 55
1000000000000000055511151231257827021181583404541015625

artinya angka persis yang disimpan di komputer sama dengan nilai desimal 0.1000000000000000055511151231257827021181583404541015625. Alih-alih menampilkan nilai desimal penuh, banyak bahasa (termasuk versi Python yang lebih lama), bulatkan hasilnya menjadi 17 digit signifikan

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

Modul fractions dan desimal membuat perhitungan ini mudah:

>>> 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'