ทำไมการเพิ่ม 0.1 หลาย ๆ ครั้งจึงไม่สูญเสีย


152

ฉันรู้ว่า0.1เลขทศนิยมไม่สามารถแสดงว่ามีจำนวนไบนารี จำกัด ( คำอธิบาย ) ดังนั้นจะสูญเสียบางอย่างแม่นยำและจะไม่ตรงdouble n = 0.1 0.1บนมืออื่น ๆสามารถแสดงว่าเพราะมันเป็น0.50.5 = 1/2 = 0.1b

ต้องบอกว่ามันเป็นที่เข้าใจว่าการเพิ่ม0.1 สามครั้งจะไม่ให้0.3รหัสดังต่อไปนี้false:

double sum = 0, d = 0.1;
for (int i = 0; i < 3; i++)
    sum += d;
System.out.println(sum == 0.3); // Prints false, OK

แต่แล้วการเพิ่ม0.1 ห้าครั้งจะให้ได้0.5อย่างไร พิมพ์รหัสต่อไปนี้true:

double sum = 0, d = 0.1;
for (int i = 0; i < 5; i++)
    sum += d;
System.out.println(sum == 0.5); // Prints true, WHY?

หาก0.1ไม่สามารถแสดงได้อย่างถูกต้องการเพิ่ม 5 ครั้งจะให้0.5สิ่งใดที่สามารถแสดงได้อย่างแม่นยำ


7
หากคุณทำการวิจัยจริง ๆ ฉันแน่ใจว่าคุณสามารถเข้าใจได้ แต่จุดลอยตัวนั้นเต็มไปด้วย "เซอร์ไพรส์" และบางครั้งก็เป็นการดีกว่าที่จะดูด้วยความสงสัย
เลียน่าสนใจ

3
คุณกำลังคิดเกี่ยวกับสิ่งนี้ในทางคณิตศาสตร์ เลขทศนิยมไม่ได้เป็นคณิตศาสตร์ แต่อย่างใด
Jakob

13
@HotLicks ที่เป็นมากมากทัศนคติที่ไม่ถูกต้องที่จะมี
ฮอบส์

2
@RussellBorogove แม้ว่าจะได้รับการปรับปรุงให้ดีที่สุดก็จะเป็นการเพิ่มประสิทธิภาพที่ถูกต้องหากsumมีค่าสุดท้ายเช่นเดียวกับถ้าวงถูกดำเนินการอย่างแท้จริง ในมาตรฐาน C ++ สิ่งนี้เรียกว่า "as-if rule" หรือ "พฤติกรรมที่สังเกตได้เหมือนกัน"
ฮอบส์

7
@ Jakob ไม่เป็นความจริง แต่อย่างใด การคำนวณเลขทศนิยมถูกกำหนดอย่างเข้มงวดด้วยการรักษาทางคณิตศาสตร์ที่ดีของขอบเขตข้อผิดพลาดและเช่น เป็นเพียงว่าโปรแกรมเมอร์จำนวนมากไม่เต็มใจที่จะติดตามการวิเคราะห์หรือพวกเขาเชื่อว่าผิดพลาดว่า "floating-point is inact" คือทั้งหมดที่ต้องรู้และการวิเคราะห์นั้นไม่คุ้มค่า
ฮอบส์

คำตอบ:


155

ข้อผิดพลาดในการปัดเศษไม่สุ่มและวิธีการใช้งานนั้นพยายามลดข้อผิดพลาดให้น้อยที่สุด ซึ่งหมายความว่าบางครั้งข้อผิดพลาดที่มองไม่เห็นหรือไม่มีข้อผิดพลาด

ยกตัวอย่างเช่น0.1จะไม่ตรง0.1เช่นnew BigDecimal("0.1") < new BigDecimal(0.1)แต่0.5เป็นว่า1.0/2

โปรแกรมนี้แสดงให้คุณเห็นคุณค่าที่แท้จริงที่เกี่ยวข้อง

BigDecimal _0_1 = new BigDecimal(0.1);
BigDecimal x = _0_1;
for(int i = 1; i <= 10; i ++) {
    System.out.println(i+" x 0.1 is "+x+", as double "+x.doubleValue());
    x = x.add(_0_1);
}

พิมพ์

0.1000000000000000055511151231257827021181583404541015625, as double 0.1
0.2000000000000000111022302462515654042363166809082031250, as double 0.2
0.3000000000000000166533453693773481063544750213623046875, as double 0.30000000000000004
0.4000000000000000222044604925031308084726333618164062500, as double 0.4
0.5000000000000000277555756156289135105907917022705078125, as double 0.5
0.6000000000000000333066907387546962127089500427246093750, as double 0.6000000000000001
0.7000000000000000388578058618804789148271083831787109375, as double 0.7000000000000001
0.8000000000000000444089209850062616169452667236328125000, as double 0.8
0.9000000000000000499600361081320443190634250640869140625, as double 0.9
1.0000000000000000555111512312578270211815834045410156250, as double 1.0

หมายเหตุ: ที่0.3ปิดเล็กน้อย แต่เมื่อคุณไป0.4ถึงบิตจะต้องเลื่อนลงหนึ่งบิตเพื่อให้พอดีกับขีด จำกัด 53 บิตและข้อผิดพลาดจะถูกยกเลิก อีกครั้งเป็นข้อผิดพลาดกลับครีพใน0.6และ0.7แต่สำหรับ0.8ที่จะ1.0เกิดข้อผิดพลาดจะถูกยกเลิก

การเพิ่มมัน 5 ครั้งควรจะสะสมข้อผิดพลาดไม่ใช่ยกเลิก

เหตุผลที่มีข้อผิดพลาดเกิดจากความแม่นยำที่ จำกัด เช่น 53-bits ซึ่งหมายความว่าในขณะที่จำนวนใช้บิตมากกว่าที่ได้รับขนาดใหญ่บิตต้องถูกปล่อยปิดท้าย ทำให้เกิดการปัดเศษซึ่งในกรณีนี้เป็นที่โปรดปรานของคุณ
คุณสามารถได้รับผลตรงกันข้ามเมื่อได้รับจำนวนที่น้อยกว่าเช่น0.1-0.0999=> 1.0000000000000286E-4 และคุณเห็นข้อผิดพลาดมากกว่าเดิม

ตัวอย่างนี้คือสาเหตุที่ใน Java 6 ทำไม Math.round (0.49999999999999994) ส่งคืน 1ในกรณีนี้การสูญเสียบิตในการคำนวณทำให้ได้คำตอบที่แตกต่างกันมาก


1
สิ่งนี้นำไปใช้ที่ไหน?
EpicPandaForce

16
@Zhuinden CPU ทำตามมาตรฐาน IEEE-754 Java ช่วยให้คุณเข้าถึงคำสั่ง CPU พื้นฐานและไม่เกี่ยวข้อง en.wikipedia.org/wiki/IEEE_floating_point
Peter Lawrey

10
@PeterLawrey: ไม่จำเป็นต้องเป็น CPU บนเครื่องที่ไม่มีจุดลอยตัวใน CPU (และไม่มีการใช้ FPU แยกต่างหาก) ซอฟต์แวร์เลขคณิต IEEE จะถูกใช้งาน และถ้าซีพียูโฮสต์มีจุดลอยตัว แต่ไม่เป็นไปตามข้อกำหนดของ IEEE ฉันคิดว่าการนำ Java ไปใช้กับ CPU นั้นจะต้องใช้ soft float เช่นกัน ...
.. GitHub STOP ช่วย ICE

1
@R .. ซึ่งในกรณีนี้ฉันไม่รู้ว่าจะเกิดอะไรขึ้นถ้าคุณใช้strictfp Time เพื่อพิจารณาจำนวนเต็มจุดคงที่ฉันคิดว่า (หรือ BigDecimal)
Peter Lawrey

2
@ eugene ปัญหาสำคัญคือค่าที่ จำกัด สามารถเป็นตัวแทนจุดลอย ข้อ จำกัด นี้อาจส่งผลให้ข้อมูลสูญหายและเมื่อจำนวนเพิ่มขึ้นการสูญเสียข้อผิดพลาด มันใช้การปัดเศษ แต่ในกรณีนี้ปัดเศษลงดังนั้นตัวเลขใดที่มีขนาดใหญ่เกินไปเล็กน้อยเนื่องจาก 0.1 มีขนาดใหญ่เกินไปเล็กน้อยจะเปลี่ยนเป็นค่าที่ถูกต้อง ตรง 0.5
Peter Lawrey

47

แบริ่งล้นในจุดลอยตัวx + x + xเป็นสิ่งที่โค้งมนอย่างถูกต้อง (เช่นที่ใกล้ที่สุด) จำนวนจุดลอยตัวไปจริง 3 * x, x + x + x + xอยู่ตรง 4 * xและx + x + x + x + xเป็นอีกครั้งประมาณอย่างถูกต้องกลมจุดลอยตัวสำหรับ 5 x*

ผลลัพธ์แรกสำหรับx + x + xมาจากความจริงที่x + xแน่นอน x + x + xดังนั้นผลลัพธ์ของการปัดเศษเพียงครั้งเดียว

ผลลัพธ์ที่สองนั้นยากขึ้นมีการพูดถึงการสาธิตอย่างหนึ่งที่นี่ (และสตีเฟ่นแคนนอนอ้างถึงการพิสูจน์อีกครั้งโดยการวิเคราะห์กรณีที่ตัวเลข 3 ตัวสุดท้ายx) เพื่อสรุปทั้ง 3 * xอยู่ในbinadeเดียวกับ 2 * xหรืออยู่ใน Binade เดียวกันกับ 4 * xและในแต่ละกรณีมีความเป็นไปได้ที่จะอนุมานว่าข้อผิดพลาดในการเพิ่มที่สามยกเลิกข้อผิดพลาดในการเพิ่มที่สอง ( นอกจากนี้ก่อนอื่นต้องแน่นอนตามที่เราได้พูดไปแล้ว)

ผลที่สาม“ x + x + x + x + xถูกปัดเศษอย่างถูกต้อง” x + xเกิดขึ้นจากสองในลักษณะเดียวกับที่เกิดขึ้นครั้งแรกจากความถูกต้องของ


ผลลัพธ์ที่สองอธิบายว่าทำไม0.1 + 0.1 + 0.1 + 0.1เป็นจำนวนจุดลอยตัว0.4: จำนวนตรรกยะ 1/10 และ 4/10 ได้ใกล้เคียงกันโดยมีข้อผิดพลาดสัมพัทธ์เดียวกันเมื่อแปลงเป็นทศนิยม ตัวเลขทศนิยมเหล่านี้มีอัตราส่วนเท่ากับ 4 ระหว่างพวกเขา ผลลัพธ์ที่หนึ่งและที่สามแสดงให้เห็นว่า0.1 + 0.1 + 0.1และ0.1 + 0.1 + 0.1 + 0.1 + 0.1สามารถคาดหวังว่าจะมีข้อผิดพลาดน้อยกว่าที่คาดการณ์ไว้โดยการวิเคราะห์ข้อผิดพลาดที่ไร้เดียงสา แต่ในตัวเองพวกเขาเกี่ยวข้องกับผลลัพธ์ตามลำดับเท่านั้น3 * 0.1และ5 * 0.1คาดว่าจะปิดและ0.30.5

หากคุณยังคงเพิ่ม0.1หลังจากการเติมครั้งที่สี่ในที่สุดคุณจะสังเกตเห็นข้อผิดพลาดในการปัดเศษที่ทำให้ " 0.1เพิ่มตัวเอง n ครั้ง" แยกออกจากn * 0.1กันและแตกต่างจาก n / 10 มากยิ่งขึ้น หากคุณต้องพล็อตค่าของ“ 0.1 เพิ่มให้กับตัวเอง n ครั้ง” เป็นฟังก์ชั่นของ n คุณจะสังเกตเห็นเส้นของความลาดชันคงที่โดย binades (ทันทีที่ผลลัพธ์ของการเติม nth ถูกกำหนดให้ตกอยู่ใน binade ที่เจาะจง คุณสมบัติของการเพิ่มสามารถคาดว่าจะคล้ายกับการเพิ่มก่อนหน้านี้ที่สร้างผลลัพธ์ใน binade เดียวกัน) ภายใน binade เดียวกันข้อผิดพลาดจะเพิ่มขึ้นหรือลดลง หากคุณดูลำดับของทางลาดจาก binade ไปยัง Binade คุณจะจำตัวเลขซ้ำของ0.1ในไบนารีในขณะที่ หลังจากนั้นการดูดซับก็จะเริ่มขึ้นและเส้นโค้งก็จะแบน


1
ในบรรทัดแรกคุณกำลังบอกว่า x + x + x นั้นถูกต้อง แต่จากตัวอย่างในคำถามมันไม่ได้
Alboz

2
@Alboz ผมบอกว่าx + x + xเป็นสิ่งที่ถูกต้องปัดเศษจำนวนจุดลอยตัวไปจริง 3 x* “ ถูกต้องกลม” หมายถึง“ ใกล้เคียงที่สุด” ในบริบทนี้
Pascal Cuoq

4
+1 นี่คือคำตอบที่ได้รับการยอมรับ จริง ๆ แล้วมันมีคำอธิบาย / หลักฐานของสิ่งที่เกิดขึ้นมากกว่าแค่ความคลุมเครือทั่วไป
.. GitHub หยุดช่วยน้ำแข็ง

1
@Alboz (ทั้งหมดนี้จินตนาการโดยคำถาม) แต่สิ่งที่คำตอบนี้อธิบายคือข้อผิดพลาดที่ยกเลิกโดยบังเอิญแทนที่จะเพิ่มขึ้นในกรณีที่เลวร้ายที่สุด
ฮอบส์

1
@chebus 0.1 คือ 0x1.999999999999999999999 … p-4 เป็นเลขฐานสิบหก (ลำดับที่ไม่มีที่สิ้นสุดของตัวเลข) ใกล้เคียงกับความแม่นยำสองเท่าเท่ากับ 0x1.99999ap-4 0.2 คือ 0x1.999999999999999999999 … p-3 ในเลขฐานสิบหก ด้วยเหตุผลเดียวกับที่ 0.1 มีค่าใกล้เคียงกับ 0x1.99999ap-4, 0.2 นั้นประมาณเป็น 0x1.99999ap-3 ในขณะเดียวกัน 0x1.99999ap-3 ก็เท่ากับ 0x1.99999ap-4 + 0x1.99999ap-4
Pascal Cuoq

-1

ระบบจุดลอยตัวทำเวทย์มนตร์ต่าง ๆ รวมถึงมีความแม่นยำเพิ่มขึ้นเล็กน้อยสำหรับการปัดเศษ ดังนั้นข้อผิดพลาดเล็ก ๆ น้อย ๆ เนื่องจากการเป็นตัวแทนที่ไม่ถูกต้องของ 0.1 จบลงด้วยการปัดเศษเป็น 0.5

คิดว่าจุดลอยตัวเป็นวิธีที่ยอดเยี่ยม แต่ INEXACT เป็นตัวแทนของตัวเลข ไม่ใช่ตัวเลขที่เป็นไปได้ทั้งหมดที่แสดงในคอมพิวเตอร์ได้อย่างง่ายดาย ตัวเลขที่ไม่มีเหตุผลเช่น PI หรือชอบ SQRT (2) (ระบบคณิตศาสตร์สัญลักษณ์สามารถเป็นตัวแทนของพวกเขา แต่ฉันพูดว่า "ง่าย")

ค่าจุดลอยตัวอาจใกล้เคียงกันมาก แต่ไม่แน่นอน อาจใกล้เคียงกับที่คุณสามารถนำทางไปยังดาวพลูโตและถูกปิดเป็นมิลลิเมตร แต่ก็ยังไม่แน่นอนในแง่คณิตศาสตร์

อย่าใช้จุดลอยเมื่อคุณจำเป็นต้องมีความแน่นอนมากกว่าโดยประมาณ ตัวอย่างเช่นแอปพลิเคชันการบัญชีต้องการติดตามจำนวนเงินที่แน่นอนในบัญชี จำนวนเต็มเป็นสิ่งที่ดีสำหรับสิ่งนั้นเพราะแน่นอน ปัญหาหลักที่คุณต้องระวังด้วยจำนวนเต็มคือล้น

การใช้ BigDecimal สำหรับสกุลเงินทำงานได้ดีเพราะการแสดงพื้นฐานเป็นจำนวนเต็มแม้ว่าจะเป็นขนาดใหญ่ก็ตาม

การรับรู้ว่าตัวเลขจุดลอยตัวนั้นไม่แน่นอนพวกเขายังคงมีประโยชน์มากมาย ระบบพิกัดสำหรับการนำทางหรือพิกัดในระบบกราฟิก ค่าทางดาราศาสตร์ คุณค่าทางวิทยาศาสตร์ (คุณอาจไม่สามารถรู้จำนวนที่แน่นอนของเบสบอลภายในมวลของอิเล็กตรอนได้ดังนั้นความไม่แน่นอนจึงไม่สำคัญจริงๆ)

สำหรับการใช้งานนับ (รวมถึงการบัญชี) ใช้จำนวนเต็ม สำหรับการนับจำนวนคนที่ผ่านประตูให้ใช้ int หรือ long


2
คำถามถูกแท็ก [java] คำจำกัดความของภาษาจาวาไม่มีข้อกำหนดสำหรับ "บิตพิเศษที่มีความแม่นยำ" เพียงไม่กี่บิตสำหรับเลขชี้กำลังพิเศษจำนวนน้อย (และนั่นก็ต่อเมื่อคุณไม่ได้ใช้strictfp) เพียงเพราะคุณละทิ้งที่จะเข้าใจบางสิ่งบางอย่างไม่ได้หมายความว่ามันไม่อาจหยั่งรู้ได้และคนอื่น ๆ ควรสละความเข้าใจ ดูstackoverflow.com/questions/18496560เป็นตัวอย่างของการนำ Java ไปใช้เพื่อกำหนดคำจำกัดความของภาษา (ซึ่งไม่รวมข้อกำหนดสำหรับบิตความแม่นยำพิเศษหรือด้วยบิตบิตstrictfpพิเศษเพิ่มเติม)
Pascal Cuoq
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.