Ini adalah pertanyaan yang saya tanyakan pada wawancara saya baru-baru ini dan saya ingin tahu (saya sebenarnya tidak ingat teori analisis numerik, jadi tolong bantu saya :)
Jika kita memiliki beberapa fungsi, yang mengakumulasi bilangan floating-point:
std::accumulate(v.begin(), v.end(), 0.0);
v
adalah std::vector<float>
, misalnya.
Apakah lebih baik mengurutkan angka-angka ini sebelum mengumpulkannya?
Urutan mana yang memberikan jawaban paling tepat?
Saya menduga bahwa mengurutkan angka dalam urutan menaik sebenarnya akan mengurangi kesalahan numerik , tetapi sayangnya saya tidak dapat membuktikannya sendiri.
PS Saya menyadari ini mungkin tidak ada hubungannya dengan pemrograman dunia nyata, hanya ingin tahu.
c++
floating-point
precision
Yippie-Ki-Yay
sumber
sumber
Jawaban:
Naluri Anda pada dasarnya benar, mengurutkan dalam urutan menaik (besarnya) biasanya sedikit meningkatkan banyak hal. Pertimbangkan kasus di mana kita menambahkan pelampung presisi tunggal (32 bit), dan ada 1 miliar nilai yang sama dengan 1 / (1 miliar), dan satu nilai sama dengan 1. Jika 1 datang lebih dulu, maka jumlahnya akan datang menjadi 1, karena 1 + (1/1 miliar) adalah 1 karena hilangnya presisi. Setiap penambahan tidak berpengaruh sama sekali pada total.
Jika nilai kecil datang lebih dulu, mereka setidaknya akan berjumlah sesuatu, meskipun demikian saya memiliki 2 ^ 30 dari mereka, sedangkan setelah 2 ^ 25 atau lebih saya kembali ke situasi di mana masing-masing secara individual tidak mempengaruhi total lagi. Jadi saya masih membutuhkan lebih banyak trik.
Itu kasus yang ekstrem, tetapi secara umum menambahkan dua nilai dengan besaran yang sama lebih akurat daripada menambahkan dua nilai dengan besaran yang sangat berbeda, karena Anda "membuang" lebih sedikit bit presisi dalam nilai yang lebih kecil dengan cara itu. Dengan mengurutkan angka-angka, Anda mengelompokkan nilai-nilai yang besarnya sama, dan dengan menambahkannya dalam urutan menaik Anda memberi nilai-nilai kecil sebuah "peluang" untuk secara kumulatif mencapai besaran angka yang lebih besar.
Namun, jika angka negatif terlibat, mudah untuk "mengecoh" pendekatan ini. Pertimbangkan tiga nilai untuk dijumlahkan
{1, -1, 1 billionth}
,. Jumlah yang benar secara aritmatika adalah1 billionth
, tetapi jika penjumlahan pertama saya melibatkan nilai kecil maka jumlah akhir saya adalah 0. Dari 6 kemungkinan pesanan, hanya 2 yang "benar" -{1, -1, 1 billionth}
dan{-1, 1, 1 billionth}
. Semua 6 pesanan memberikan hasil yang akurat pada skala nilai besaran terbesar di masukan (0,0000001% keluar), tetapi untuk 4 dari mereka hasilnya tidak akurat pada skala solusi sebenarnya (100% keluar). Masalah khusus yang Anda selesaikan akan memberi tahu Anda apakah yang pertama cukup baik atau tidak.Faktanya, Anda dapat memainkan lebih banyak trik daripada hanya menambahkannya dalam urutan yang diurutkan. Jika Anda memiliki banyak nilai yang sangat kecil, angka tengah dari nilai sedang, dan sejumlah kecil nilai besar, maka mungkin paling akurat untuk pertama-tama menjumlahkan semua yang kecil, lalu secara terpisah menjumlahkan yang sedang, tambahkan kedua total tersebut bersama-sama lalu tambahkan yang besar. Sama sekali tidak sepele untuk menemukan kombinasi paling akurat dari penambahan floating-point, tetapi untuk mengatasi kasus yang sangat buruk, Anda dapat menyimpan seluruh rangkaian total yang berjalan pada besaran yang berbeda, tambahkan setiap nilai baru ke total yang paling sesuai dengan besarnya, dan saat total berjalan mulai terlalu besar untuk besarannya, tambahkan ke total berikutnya dan mulai yang baru. Diambil ke ekstrem logisnya, proses ini setara dengan melakukan penjumlahan dalam tipe presisi sewenang-wenang (jadi Anda ' d melakukan itu). Tetapi mengingat pilihan sederhana untuk menambahkan dalam urutan naik atau turun, naik adalah taruhan yang lebih baik.
Ini memang memiliki beberapa hubungan dengan pemrograman dunia nyata, karena ada beberapa kasus di mana perhitungan Anda bisa menjadi sangat salah jika Anda secara tidak sengaja memotong ekor "berat" yang terdiri dari sejumlah besar nilai yang masing-masing terlalu kecil untuk mempengaruhi satu per satu. jumlahnya, atau jika Anda membuang terlalu banyak presisi dari banyak nilai kecil yang secara individual hanya memengaruhi beberapa bit terakhir dari jumlah tersebut. Dalam kasus di mana ekor dapat diabaikan, Anda mungkin tidak peduli. Misalnya jika Anda hanya menjumlahkan sejumlah kecil nilai di tempat pertama dan Anda hanya menggunakan beberapa angka penting dari jumlah tersebut.
sumber
Ada juga algoritma yang dirancang untuk operasi akumulasi semacam ini, yang disebut Penjumlahan Kahan , yang mungkin harus Anda ketahui.
Menurut Wikipedia,
sumber
sum
danc
dengan besaran yang berbeda. Ini dapat diperpanjang dengan mudah ke variabel N.-ffast-math
di GCC).-ffast-math
. Apa yang saya pelajari dari diskusi ini dan tautan ini , adalah bahwa jika Anda peduli dengan keakuratan numerik, Anda mungkin harus menghindari penggunaan-ffast-math
tetapi di banyak aplikasi di mana Anda mungkin terikat CPU tetapi tidak peduli dengan perhitungan numerik yang tepat, (pemrograman game misalnya ),-ffast-math
wajar untuk digunakan. Karena itu, saya ingin mengubah komentar saya yang "dilarang".sum, c, t, y
akan membantu. Anda juga perlu menambahkansum -= c
sebelumnyareturn sum
.Saya mencoba contoh ekstrim dalam jawaban yang diberikan oleh Steve Jessop.
Saya mendapatkan hasil sebagai berikut:
Kesalahan di baris pertama lebih dari sepuluh kali lebih besar di baris kedua.
Jika saya mengubah
double
s menjadifloat
s pada kode di atas, saya mendapatkan:Tidak ada jawaban yang bahkan mendekati 2.0 (tetapi yang kedua sedikit lebih dekat).
Menggunakan penjumlahan Kahan (dengan
double
s) seperti yang dijelaskan oleh Daniel Pryden:Saya mendapatkan persis 2.0:
Dan bahkan jika saya mengubah
double
s menjadifloat
s pada kode di atas, saya mendapatkan:Tampaknya Kahan adalah jalan yang harus ditempuh!
sumber
double
tidak buruk kehilangan ketepatan dalam menambahkan bersama-sama satu miliar miliar, karena ia memiliki 52 bit signifikan, sedangkan IEEEfloat
hanya memiliki 24 dan akan.c
untuk memuat nilai yang jauh lebih besar daripada ringkasan berikutnya. Ini berarti jumlah penjumlahannya jauh, jauh lebih kecil daripada jumlah utama, jadi harus ada banyak sekali untuk dijumlahkan. Apalagi dengandouble
aritmatika.Ada kelas algoritme yang menyelesaikan masalah ini secara tepat, tanpa perlu mengurutkan atau menyusun ulang data .
Dengan kata lain, penjumlahan dapat dilakukan dalam satu kali lintasan data. Hal ini juga membuat algoritme semacam itu dapat diterapkan dalam situasi di mana kumpulan data tidak diketahui sebelumnya, misalnya jika data tiba dalam waktu nyata dan jumlah yang berjalan perlu dipertahankan.
Berikut adalah abstrak makalah terbaru:
Sumber: Algoritma 908: Penjumlahan Tepat Online Arus Titik Mengambang .
sumber
Berdasarkan jawaban Steve untuk pertama-tama mengurutkan angka-angka dalam urutan menaik, saya akan memperkenalkan dua gagasan lagi:
Tentukan perbedaan eksponen dua angka di atas yang mungkin Anda putuskan bahwa Anda akan kehilangan terlalu banyak presisi.
Kemudian tambahkan angkanya secara berurutan hingga eksponen akumulator terlalu besar untuk bilangan berikutnya, lalu letakkan akumulator ke antrean sementara dan mulai akumulator dengan bilangan berikutnya. Lanjutkan sampai Anda kehabisan daftar aslinya.
Anda mengulangi proses dengan antrian sementara (setelah mengurutkannya) dan dengan perbedaan eksponen yang mungkin lebih besar.
Saya rasa ini akan sangat lambat jika Anda harus menghitung eksponen sepanjang waktu.
Saya dengan cepat pergi dengan sebuah program dan hasilnya adalah 1,99903
sumber
Saya pikir Anda bisa melakukan lebih baik daripada menyortir angka sebelum Anda mengumpulkannya, karena selama proses akumulasi, akumulator menjadi semakin besar. Jika Anda memiliki banyak angka serupa, Anda akan mulai kehilangan presisi dengan cepat. Inilah yang saya sarankan:
Tentu saja algoritma ini akan paling efisien dengan antrian prioritas daripada daftar. Kode C ++:
sopir:
Angka dalam antrian negatif karena
top
menghasilkan angka terbesar , tetapi kita menginginkan yang terkecil . Saya bisa memberikan lebih banyak argumen template ke antrian, tetapi pendekatan ini tampaknya lebih sederhana.sumber
Ini tidak cukup menjawab pertanyaan Anda, tetapi hal yang cerdas untuk dilakukan adalah menjalankan penjumlahan dua kali, sekali dengan mode pembulatan " pembulatan ke atas" dan sekali dengan "pembulatan ke bawah". Bandingkan kedua jawaban tersebut, dan Anda tahu / bagaimana / tidak akurat hasil Anda, dan oleh karena itu Anda perlu menggunakan strategi penjumlahan yang lebih cerdas. Sayangnya, sebagian besar bahasa tidak membuat perubahan mode pembulatan floating point semudah yang seharusnya, karena orang tidak tahu bahwa itu sebenarnya berguna dalam perhitungan sehari-hari.
Lihatlah aritmatika Interval di mana Anda melakukan semua matematika seperti ini, menjaga nilai tertinggi dan terendah saat Anda pergi. Ini mengarah pada beberapa hasil dan optimisasi yang menarik.
sumber
Yang paling sederhana semacam yang meningkatkan akurasi untuk mengurutkan berdasarkan nilai absolut naik. Itu memungkinkan nilai magnitudo terkecil memiliki kesempatan untuk mengakumulasi atau membatalkan sebelum berinteraksi dengan nilai magnitudo yang lebih besar yang akan memicu hilangnya presisi.
Meskipun demikian, Anda dapat melakukan lebih baik dengan melacak beberapa jumlah parsial yang tidak tumpang tindih. Berikut adalah makalah yang menjelaskan teknik dan menyajikan bukti akurasi: www-2.cs.cmu.edu/afs/cs/project/quake/public/papers/robust-arithmetic.ps
Algoritme tersebut dan pendekatan lain untuk penjumlahan titik mengambang yang tepat diimplementasikan dengan Python sederhana di: http://code.activestate.com/recipes/393090/ Setidaknya dua di antaranya dapat diubah dengan mudah ke C ++.
sumber
Untuk IEEE 754 presisi tunggal atau ganda atau nomor format yang diketahui, alternatif lain adalah menggunakan larik angka (diteruskan oleh pemanggil, atau dalam kelas untuk C ++) yang diindeks oleh eksponen. Saat menambahkan angka ke dalam array, hanya angka dengan eksponen yang sama yang ditambahkan (sampai slot kosong ditemukan dan angka disimpan). Saat penjumlahan diminta, larik dijumlahkan dari terkecil ke terbesar untuk meminimalkan pemotongan. Contoh presisi tunggal:
contoh presisi ganda:
sumber
Pelampung Anda harus ditambahkan dengan presisi ganda. Itu akan memberi Anda lebih banyak presisi tambahan daripada teknik lainnya. Untuk presisi yang lebih tinggi dan kecepatan yang jauh lebih tinggi, Anda dapat membuat katakan empat penjumlahan, dan menjumlahkannya di akhir.
Jika Anda menambahkan angka presisi ganda, gunakan double panjang untuk penjumlahannya - namun, ini hanya akan berdampak positif dalam implementasi di mana long double sebenarnya memiliki presisi lebih dari double (biasanya x86, PowerPC bergantung pada pengaturan compiler).
sumber
Mengenai pengurutan, menurut saya, jika Anda mengharapkan pembatalan maka angka-angka tersebut harus ditambahkan dalam urutan besaran turun , bukan naik. Misalnya:
((-1 + 1) + 1e-20) akan menghasilkan 1e-20
tapi
((1e-20 + 1) - 1) akan menghasilkan 0
Dalam persamaan pertama, dua bilangan besar ditiadakan, sedangkan pada persamaan kedua suku 1e-20 hilang jika ditambahkan ke 1, karena tidak cukup presisi untuk mempertahankannya.
Selain itu, penjumlahan berpasangan cukup baik untuk menjumlahkan banyak angka.
sumber