วิธีแก้ปัญหาข้อผิดพลาดในการปัดเศษทศนิยม


18

ในการสร้างแอปพลิเคชั่นที่เกี่ยวข้องกับการคำนวณทางคณิตศาสตร์จำนวนมากฉันได้พบปัญหาว่าตัวเลขบางตัวทำให้เกิดข้อผิดพลาดในการปัดเศษ

ในขณะที่ฉันเข้าใจว่าจุดลอยไม่แน่นอนปัญหาคือวิธีการที่ฉันจะจัดการกับตัวเลขที่แน่นอนที่จะทำให้แน่ใจว่าเมื่อคำนวณจะ preformed ที่พวกเขาลอยปัดเศษจุดไม่ก่อให้เกิดปัญหาใด ๆ ?


2
มีปัญหาเฉพาะที่คุณเผชิญหรือไม่? มีหลายวิธีในการทำการทดสอบซึ่งเหมาะสำหรับปัญหาบางอย่าง คำถามที่มีหลายคำตอบนั้นไม่เหมาะสมสำหรับรูปแบบคำถาม & คำตอบ มันจะดีที่สุดถ้าคุณสามารถกำหนดปัญหาที่คุณมีในแบบที่อาจมีคำตอบที่ถูกต้องมากกว่าที่จะเสนอความคิดเห็นและคำแนะนำ

ฉันกำลังสร้างแอปพลิเคชันซอฟต์แวร์ที่มีการคำนวณทางคณิตศาสตร์จำนวนมาก ฉันเข้าใจการทดสอบ NUNIT หรือ JUNIT จะดี แต่ชอบที่จะมีความคิดเกี่ยวกับวิธีการแก้ไขปัญหาด้วยการคำนวณทางคณิตศาสตร์
JNL

1
คุณสามารถยกตัวอย่างการคำนวณที่คุณจะทำการทดสอบได้หรือไม่? โดยทั่วไปแล้วจะไม่เป็นการทดสอบหน่วยทางคณิตศาสตร์ (เว้นแต่ว่าคุณกำลังทดสอบประเภทตัวเลขของคุณเอง) แต่การทดสอบบางอย่างdistanceTraveled(startVel, duration, acceleration)จะถูกทดสอบ

ตัวอย่างหนึ่งจะจัดการกับจุดทศนิยม ตัวอย่างเช่นสมมติว่าเรากำลังสร้างกำแพงที่มีการตั้งค่าพิเศษสำหรับ dist x-0 ถึง x = 14.589 แล้วการจัดเรียงบางส่วนจาก x = 14.589 ถึง x = จุดสิ้นสุดของกำแพง ระยะทาง. 589 เมื่อแปลงเป็นไบนารี่ไม่เท่ากันโดยเฉพาะถ้าเราเพิ่มระยะทาง ... เช่น 14.589 + 0.25 จะไม่เท่ากับ 14.84 ในไบนารี่ .... ฉันหวังว่ามันจะไม่สับสนใช่ไหม
JNL

1
@MichaelT ขอบคุณสำหรับการแก้ไขคำถาม ช่วยได้มาก ตั้งแต่ยังใหม่กับสิ่งนี้ไม่ดีเกินไปในการวางกรอบคำถาม :) ... แต่จะดีในไม่ช้า
JNL

คำตอบ:


22

มีวิธีพื้นฐานสามประการในการสร้างประเภทตัวเลขทางเลือกที่ไม่มีการปัดเศษทศนิยม ชุดรูปแบบทั่วไปของสิ่งเหล่านี้คือพวกเขาใช้เลขจำนวนเต็มแทนในรูปแบบต่างๆ

rationals

แสดงตัวเลขเป็นทั้งส่วนและจำนวนตรรกยะที่มีตัวเศษและส่วน จำนวน15.589จะถูกแทนw: 15; n: 589; d:1000ด้วย

เมื่อเพิ่มเข้าไปที่ 0.25 (ซึ่งก็คือw: 0; n: 1; d: 4) สิ่งนี้เกี่ยวข้องกับการคำนวณ LCM แล้วเพิ่มตัวเลขสองตัว วิธีนี้ใช้งานได้ดีในหลาย ๆ สถานการณ์ แต่อาจส่งผลให้มีจำนวนมากเมื่อคุณทำงานกับจำนวนตรรกยะจำนวนมากซึ่งค่อนข้างดีสำหรับกันและกัน

จุดคงที่

คุณมีทั้งส่วนและส่วนทศนิยม ตัวเลขทั้งหมดถูกปัดเศษ (มีคำนั้น - แต่คุณรู้ว่าอยู่ตรงไหน) ของความแม่นยำนั้น ตัวอย่างเช่นคุณอาจมีจุดคงที่ที่มีทศนิยม 3 ตำแหน่ง 15.589+ 0.250จะเพิ่ม589 + 250 % 1000สำหรับส่วนทศนิยม (แล้วดำเนินการใด ๆ กับส่วนทั้งหมด) มันทำงานได้ดีมากกับฐานข้อมูลที่มีอยู่ ดังที่กล่าวไว้มีการปัดเศษ แต่คุณทราบว่าอยู่ที่ไหนและสามารถระบุได้ว่าแม่นยำกว่าที่ต้องการ (คุณวัดได้เพียง 3 จุดทศนิยมดังนั้นให้แก้ไข 4)

จุดลอยตัวคงที่

เก็บค่าและความแม่นยำ 15.589ถูกเก็บไว้เป็น15589ค่าและ3เพื่อความแม่นยำในขณะที่0.25ถูกเก็บไว้เป็นและ25 2สิ่งนี้สามารถจัดการกับความแม่นยำโดยพลการ ฉันเชื่อว่านี่คือสิ่งที่ internals ของ BigDecimal ใช้ (ไม่ได้ดูเมื่อเร็ว ๆ นี้) ใช้ เมื่อถึงจุดหนึ่งคุณจะต้องเอามันกลับมาจากรูปแบบนี้และแสดงมัน - และนั่นอาจเกี่ยวข้องกับการปัดเศษ (อีกครั้งคุณควบคุมว่ามันอยู่ที่ไหน)


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


2
เป็นการเริ่มต้นที่ดี แต่แน่นอนว่ามันไม่สามารถแก้ปัญหาการปัดเศษได้อย่างสมบูรณ์ ตัวเลขที่ไม่ลงตัวเช่นπ, eและ√2ไม่มีการแสดงตัวเลขอย่างเคร่งครัด คุณจำเป็นต้องแสดงสัญลักษณ์เหล่านั้นหากคุณต้องการการแสดงที่ถูกต้องหรือประเมินพวกเขาให้เร็วที่สุดถ้าคุณต้องการลดข้อผิดพลาดในการปัดเศษให้น้อยที่สุด
คาเลบ

@Caleb สำหรับ irrationals อย่างใดอย่างหนึ่งจะต้องประเมินพวกเขาไปไกลเกินกว่าที่การปัดเศษใด ๆ อาจทำให้เกิดปัญหา ตัวอย่างเช่น 22/7 มีความแม่นยำถึง 0.1% ของ pi, 355/113 มีความแม่นยำถึง 10 ^ -8 หากคุณทำงานกับตัวเลขถึงทศนิยม 3 ตำแหน่งการมี 3.141592653 ควรหลีกเลี่ยงข้อผิดพลาดในการปัดเศษที่ทศนิยม 3 ตำแหน่ง

@MichaelT: สำหรับการเพิ่มจำนวนตรรกยะคุณไม่จำเป็นต้องค้นหา LCM และเร็วกว่าที่จะไม่ (และเร็วกว่าในการยกเลิก "ศูนย์ LSB" หลังจากนั้นและลดความซับซ้อนลงอย่างมากเมื่อจำเป็นจริงๆเท่านั้น) สำหรับตัวเลขที่มีเหตุผลโดยทั่วไปเป็นเพียง "ตัวเศษ / ส่วน" เพียงอย่างเดียวหรือ "ตัวเศษ / ตัวส่วน << เลขยกกำลัง" (ไม่ใช่ "ทั้งส่วน + ตัวเศษ / ส่วน") อีกทั้ง "จุดลอยตัวคงที่" ของคุณคือการแสดงจุดลอยตัวและจะอธิบายได้ดีกว่าว่า "จุดลอยตัวขนาดตามอำเภอใจ" (เพื่อแยกความแตกต่างจาก
เบรนแดน

คำศัพท์บางคำของคุณค่อนข้างแน่นอน - จุดตรึงลอยตัวไม่สมเหตุสมผล - ฉันคิดว่าคุณกำลังพยายามพูดทศนิยมทศนิยม
jk

10

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

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


ฉันกำลังใช้ VC ++ ตอนนี้ ... แต่ฉันจะขอบคุณข้อมูลเพิ่มเติมเกี่ยวกับภาษาการเขียนโปรแกรมอื่น ๆ ด้วย
JNL

แม้จะไม่มีค่าจุดลอยตัวก็ตามคุณยังคงพบกับปัญหารอบตัว
ชาด

2
@Chad True แต่เป้าหมายไม่ใช่เพื่อขจัดปัญหาการปัดเศษ (ซึ่งจะมีอยู่เสมอเพราะในฐานใดก็ตามที่คุณใช้มีตัวเลขบางตัวที่ไม่มีการแทนที่แน่นอนและคุณไม่มีหน่วยความจำและการประมวลผลที่ไม่มีขีด จำกัด ) ลดให้เหลือน้อยที่สุดจนถึงไม่มีผลในการคำนวณที่คุณพยายามทำ
Iker

@Iker คุณพูดถูก แม้ว่าคุณหรือบุคคลที่ถามคำถามจะไม่ได้ระบุว่าการคำนวณแบบใดที่พวกเขาพยายามจะบรรลุและความแม่นยำที่พวกเขาต้องการ เขาต้องตอบคำถามก่อนก่อนที่จะกระโดดปืนเข้าสู่ทฤษฎีจำนวน เพียงแค่พูดว่าlot of mathematical calculationsไม่เป็นประโยชน์หรือคำตอบที่ได้รับ ในกรณีส่วนใหญ่ (ถ้าคุณไม่ได้จัดการกับสกุลเงิน) การลอยตัวก็น่าจะเพียงพอแล้ว
ชาด

@Chad นั่นเป็นจุดที่ยุติธรรมมีข้อมูลไม่เพียงพอจาก OP เพื่อบอกระดับความแม่นยำที่พวกเขาต้องการ
Iker

7

เลขทศนิยมมักจะแม่นยำมาก (15 หลักทศนิยมสำหรับ a double) และค่อนข้างยืดหยุ่น ปัญหาเกิดขึ้นเมื่อคุณทำคณิตศาสตร์ซึ่งจะช่วยลดจำนวนของความแม่นยำ นี่คือตัวอย่างบางส่วน:

  • การยกเลิกการลบ: 1234567890.12345 - 1234567890.12300ผลลัพธ์0.0045มีความแม่นยำเพียงสองหลัก การโจมตีนี้จะเกิดขึ้นทุกครั้งที่คุณลบจำนวนที่มีขนาดใกล้เคียงกัน

  • การกลืนความแม่นยำ: 1234567890.12345 + 0.123456789012345ประเมิน1234567890.24691ว่าตัวเลขสิบตัวสุดท้ายของตัวถูกดำเนินการตัวที่สองหายไป

  • การคูณ: ถ้าคุณคูณสอง 15 หลักผลลัพธ์จะมี 30 หลักที่จะต้องเก็บไว้ แต่คุณไม่สามารถเก็บพวกมันได้ดังนั้น 15 บิตสุดท้ายจะหายไป นี่เป็นเรื่องน่ารำคาญอย่างยิ่งเมื่อรวมกับsqrt()(ในsqrt(x*x + y*y): ผลลัพธ์จะมีความแม่นยำ 7.5 หลักเท่านั้น

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

for(double f = f0; f < f1; f += df) {

หลังจากนั้นไม่กี่ซ้ำที่มีขนาดใหญ่จะกลืนเป็นส่วนหนึ่งของความแม่นยำของf dfยิ่งไปกว่านั้นข้อผิดพลาดจะเพิ่มขึ้นซึ่งนำไปสู่สถานการณ์ที่ขัดแย้งได้ว่าขนาดเล็กdfอาจนำไปสู่ผลลัพธ์โดยรวมที่แย่ลง ดีกว่าเขียนสิ่งนี้:

for(int i = 0; i < (f1 - f0)/df; i++) {
    double f = f0 + i*df;

เนื่องจากคุณกำลังรวมการเพิ่มขึ้นในการคูณเดียวผลลัพธ์fจะแม่นยำทศนิยม 15 หลัก

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


2

วิธีตรวจสอบให้แน่ใจว่าคุณไม่มีปัญหา: เรียนรู้เกี่ยวกับปัญหาเลขคณิตจุดลอยตัวหรือจ้างคนที่ทำหรือใช้สามัญสำนึกบางอย่าง

ปัญหาแรกคือความแม่นยำ ในหลายภาษาคุณมี "float" และ "double" (ยืนสองเท่าสำหรับ "double precision") และในหลาย ๆ กรณี "float" ให้คุณมีความแม่นยำ 7 หลักในขณะที่ double ให้คุณ 15. สามัญสำนึกคือถ้าคุณมี สถานการณ์ที่ความแม่นยำอาจเป็นปัญหา 15 หลักคือความน่ากลัวมากกว่า 7 หลัก ในสถานการณ์ที่มีปัญหาเล็กน้อยหลายอย่างการใช้ "double" หมายถึงคุณไม่ได้อยู่กับมันและ "float" หมายความว่าคุณไม่ได้ทำ สมมติว่าตลาดของ บริษัท อยู่ที่ 700 พันล้านดอลลาร์ เป็นตัวแทนนี้ในลอยและบิตต่ำสุดคือ $ 65536 เป็นตัวแทนของมันโดยใช้สองครั้งและบิตที่ต่ำที่สุดคือประมาณ 0.012 เซนต์ ดังนั้นถ้าคุณไม่รู้ว่าคุณกำลังทำอะไรอยู่คุณใช้สองเท่าไม่ใช่ลอย

ปัญหาที่สองเป็นเรื่องของหลักการมากกว่า หากคุณทำการคำนวณสองแบบที่แตกต่างกันซึ่งควรให้ผลลัพธ์เดียวกันพวกเขามักจะไม่เกิดข้อผิดพลาดในการปัดเศษ ผลลัพธ์สองรายการที่ควรจะเท่ากันจะเป็น "เกือบเท่ากัน" หากผลลัพธ์ทั้งสองอยู่ใกล้กันค่าจริงอาจเท่ากัน หรือพวกเขาอาจจะไม่ คุณต้องจำไว้และควรเขียนและใช้ฟังก์ชั่นที่พูดว่า "x มากกว่า y" หรือ "x น้อยกว่า y" หรือ "x และ y อาจเท่ากับ"

ปัญหานี้ยิ่งแย่ลงมากถ้าคุณใช้การปัดเศษตัวอย่างเช่น "round x ลงไปเป็นจำนวนเต็มที่ใกล้เคียงที่สุด" หากคุณคูณ 120 * 0.05 ผลลัพธ์ควรเป็น 6 แต่สิ่งที่คุณได้รับคือ "มีจำนวนใกล้เคียงกับ 6" หากคุณ "ปัดเศษเป็นจำนวนเต็มที่ใกล้ที่สุด" นั่นคือ "ตัวเลขที่ใกล้มากถึง 6" อาจเป็น "น้อยกว่า 6" เล็กน้อยและได้รับการปัดเศษเป็น 5 และโปรดทราบว่ามันไม่สำคัญว่าคุณมีความแม่นยำเท่าใด ไม่สำคัญว่าผลลัพธ์ของคุณจะใกล้เคียงกับ 6 เท่าใดตราบใดที่น้อยกว่า 6

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


1
ไม่มีอะไร "สามัญสำนึก" เกี่ยวกับคณิตศาสตร์จุดลอย
whatsisname

เรียนรู้เพิ่มเติมเกี่ยวกับมัน
gnasher729

0

คนส่วนใหญ่ทำผิดเมื่อพวกเขาเห็นสองครั้งพวกเขากรีดร้อง BigDecimal เมื่อพวกเขาเพิ่งย้ายปัญหาไปที่อื่น Double ให้ Sign bit: 1 bit, Exponent width: 11 bits ความแม่นยำและความสำคัญ: 53 บิต (เก็บไว้ 52 อย่างชัดเจน) เนื่องจากลักษณะของ double, interger ทั้งหมดที่มีขนาดใหญ่กว่าคุณสูญเสียความแม่นยำสัมพัทธ์ ในการคำนวณความแม่นยำสัมพัทธ์ที่เราใช้ในที่นี้คือการร้อง

ความแม่นยำสัมพัทธ์ของสองเท่าในการคำนวณเราใช้ foluma ต่อไปนี้ 2 ^ E <= abs (X) <2 ^ (E + 1)

epsilon = 2 ^ (E-10)% สำหรับทศนิยม 16 บิต (ความแม่นยำครึ่งหนึ่ง)

 Accuracy Power | Accuracy -/+| Maximum Power | Max Interger Value
 2^-1           | 0.5         | 2^51          | 2.2518E+15
 2^-5           | 0.03125     | 2^47          | 1.40737E+14
 2^-10          | 0.000976563 | 2^42          | 4.39805E+12
 2^-15          | 3.05176E-05 | 2^37          | 1.37439E+11
 2^-20          | 9.53674E-07 | 2^32          | 4294967296
 2^-25          | 2.98023E-08 | 2^27          | 134217728
 2^-30          | 9.31323E-10 | 2^22          | 4194304
 2^-35          | 2.91038E-11 | 2^17          | 131072
 2^-40          | 9.09495E-13 | 2^12          | 4096
 2^-45          | 2.84217E-14 | 2^7           | 128
 2^-50          | 8.88178E-16 | 2^2           | 4

กล่าวอีกนัยหนึ่งถ้าคุณต้องการความแม่นยำ +/- 0.5 (หรือ 2 ^ -1) ขนาดสูงสุดที่สามารถเป็นตัวเลขได้คือ 2 ^ 52 ยิ่งใหญ่กว่านี้และระยะห่างระหว่างตัวเลขจุดลอยตัวมากกว่า 0.5

หากคุณต้องการความแม่นยำ +/- 0.0005 (ประมาณ 2 ^ -11) ขนาดสูงสุดที่สามารถเป็นตัวเลขได้คือ 2 ^ 42 ยิ่งใหญ่กว่านี้และระยะห่างระหว่างหมายเลขจุดลอยตัวมากกว่า 0.0005

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

แม้ว่ามันจะเป็นสิ่งที่จะกล่าวถ้าคุณเพียง แต่ตั้งใจที่จะจำลองโลก 100 เมตรโดย 100 เมตรคุณจะมีที่ไหนสักแห่งในความถูกต้องใกล้ 2 ^ -45 สิ่งนี้ไม่ได้รวมไปถึงความทันสมัยของ FPU ภายในซีพียูที่จะทำการคำนวณนอกขนาดของประเภทเนทีฟและหลังจากการคำนวณเสร็จสมบูรณ์พวกเขาจะทำการปัดเศษ (ขึ้นอยู่กับโหมดการปัดเศษ FPU) กับขนาดของเนทีฟ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.