Giải pháp cho lỗi làm tròn điểm nổi


18

Khi xây dựng một ứng dụng liên quan đến nhiều phép tính toán học, tôi đã gặp phải vấn đề là một số con số gây ra lỗi làm tròn.

Mặc dù tôi hiểu rằng dấu phẩy động không chính xác , nhưng vấn đề là làm thế nào để tôi xử lý các con số chính xác để đảm bảo rằng khi các phép tính được tạo thành trước chúng, làm tròn điểm nổi không gây ra vấn đề gì?


2
Có một vấn đề cụ thể bạn đang phải đối mặt? Có nhiều cách để kiểm tra, tất cả đều đúng cho một số vấn đề. Các câu hỏi có thể có nhiều câu trả lời không phù hợp với định dạng Hỏi và Đáp. Sẽ là tốt nhất nếu bạn có thể xác định vấn đề bạn đang gặp phải theo cách có thể có một câu trả lời đúng hơn là tạo một mạng lưới cho các ý tưởng và đề xuất.

Tôi đang xây dựng một Ứng dụng phần mềm với rất nhiều phép toán. Tôi hiểu rằng thử nghiệm NUNIT hoặc JUNIT sẽ tốt, nhưng rất thích có ý tưởng về cách tiếp cận các vấn đề với Phép toán toán học.
JNL

1
Bạn có thể đưa ra một ví dụ về một tính toán bạn sẽ kiểm tra? Thông thường người ta sẽ không kiểm tra đơn vị toán học thô (trừ khi bạn đang kiểm tra các kiểu số của riêng mình), nhưng kiểm tra một cái gì đó giống như distanceTraveled(startVel, duration, acceleration)sẽ được kiểm tra.

Một ví dụ sẽ là xử lý điểm thập phân. Ví dụ: giả sử chúng tôi đang xây dựng một bức tường với các cài đặt đặc biệt cho dist x-0 đến x = 14,589 và sau đó một số sắp xếp từ x = 14,589 đến x = cuối bức tường. Khoảng cách 0,55 khi chuyển đổi thành nhị phân không giống nhau .... Đặc biệt nếu chúng ta thêm một số khoảng cách ... như 14,589 + 0,25 sẽ không bằng 14,84 trong nhị phân .... Tôi hy vọng nó không khó hiểu?
JNL

1
@MichaelT cảm ơn bạn đã chỉnh sửa Câu hỏi. Giúp rất nhiều. Vì mới làm quen với điều này, không quá tốt về cách đóng khung các câu hỏi. :) ... Nhưng sẽ tốt thôi.
JNL

Câu trả lời:


22

Có ba cách tiếp cận cơ bản để tạo ra các kiểu số thay thế không có làm tròn điểm nổi. Chủ đề chung với những điều này là họ sử dụng toán học số nguyên thay vì theo nhiều cách khác nhau.

Hợp lý

Biểu diễn số dưới dạng toàn bộ phần và số hữu tỉ bằng tử số và mẫu số. Số 15.589sẽ được đại diện là w: 15; n: 589; d:1000.

Khi được thêm vào 0,25 (nghĩa là w: 0; n: 1; d: 4), điều này liên quan đến việc tính toán LCM, sau đó thêm hai số. Điều này hoạt động tốt trong nhiều tình huống, mặc dù có thể dẫn đến số lượng rất lớn khi bạn đang làm việc với nhiều số hữu tỷ tương đối chính với nhau.

Điểm cố định

Bạn có toàn bộ phần, và phần thập phân. Tất cả các số được làm tròn (có từ đó - nhưng bạn biết nó ở đâu) với độ chính xác đó. Ví dụ: bạn có thể có điểm cố định với 3 điểm thập phân. 15.589+ 0.250trở thành thêm 589 + 250 % 1000cho phần thập phân (và sau đó bất kỳ thực hiện cho toàn bộ phần). Điều này hoạt động rất độc đáo với cơ sở dữ liệu hiện có. Như đã đề cập, có làm tròn nhưng bạn biết nó ở đâu và có thể chỉ định nó sao cho chính xác hơn mức cần thiết (bạn chỉ đo đến 3 điểm thập phân, vì vậy hãy sửa nó 4).

Điểm cố định nổi

Lưu trữ một giá trị và độ chính xác. 15.589được lưu trữ như 15589đối với giá trị và 3độ chính xác, trong khi 0.25được lưu trữ dưới dạng 252. Điều này có thể xử lý chính xác tùy ý. Tôi tin rằng đây là những gì bên trong sử dụng BigDecimal của Java (gần đây chưa xem xét). Tại một số điểm, bạn sẽ muốn đưa nó trở lại định dạng này và hiển thị nó - và điều đó có thể liên quan đến làm tròn (một lần nữa, bạn kiểm soát vị trí của nó).


Khi bạn xác định lựa chọn cho đại diện, bạn có thể tìm thấy các thư viện bên thứ ba hiện có sử dụng thư viện này hoặc viết thư viện của riêng bạn. Khi tự viết, hãy chắc chắn để kiểm tra đơn vị và đảm bảo rằng bạn đang làm toán chính xác.


2
Đó là một khởi đầu tốt, nhưng tất nhiên nó không giải quyết được hoàn toàn vấn đề làm tròn. Các số vô tỷ như π, e và √2 không có biểu diễn số nghiêm ngặt; bạn cần đại diện cho chúng một cách tượng trưng nếu bạn muốn một đại diện chính xác, hoặc đánh giá chúng càng muộn càng tốt nếu bạn chỉ muốn giảm thiểu lỗi làm tròn.
Caleb

@Caleb cho những điều phi lý người ta sẽ cần phải đánh giá chúng để vượt ra ngoài bất kỳ việc làm tròn nào có thể gây ra vấn đề. Ví dụ: 22/7 chính xác đến 0,1% số pi, 355/113 chính xác đến 10 ^ -8. Nếu bạn chỉ làm việc với các số đến 3 chữ số thập phân, có 3.141592653 nên tránh mọi lỗi làm tròn ở 3 chữ số thập phân.

@MichaelT: Ngoài các số hữu tỷ, bạn không cần tìm LCM và không nhanh hơn (và nhanh hơn để hủy "số không LSB" sau đó, và chỉ đơn giản hóa hoàn toàn khi thực sự cần thiết). Đối với các số hữu tỷ nói chung, thông thường chỉ là "tử số / mẫu số" hoặc "tử số / mẫu số << số mũ" (chứ không phải "toàn bộ phần + tử số / mẫu số"). Ngoài ra, "điểm cố định nổi" của bạn là một đại diện điểm nổi và sẽ được mô tả tốt hơn là "điểm nổi kích thước tùy ý" (để phân biệt với "điểm nổi kích thước cố định").
Brendan

một số thuật ngữ của bạn là một chút iffy - điểm cố định trôi nổi không có ý nghĩa - tôi nghĩ bạn đang cố gắng nói số thập phân trôi nổi.
jk.

10

Nếu các giá trị dấu phẩy động có vấn đề làm tròn và bạn không muốn gặp phải các vấn đề làm tròn, thì theo logic, cách duy nhất là không sử dụng các giá trị dấu phẩy động.

Bây giờ câu hỏi trở thành, "làm thế nào để tôi làm toán liên quan đến các giá trị không nguyên mà không có biến dấu phẩy động?" Câu trả lời là với các loại dữ liệu chính xác tùy ý . Tính toán chậm hơn vì chúng phải được thực hiện trong phần mềm thay vì trong phần cứng, nhưng chúng chính xác. Bạn không nói ngôn ngữ nào bạn đang sử dụng, vì vậy tôi không thể đề xuất một gói, nhưng có các thư viện chính xác tùy ý có sẵn cho hầu hết các ngôn ngữ lập trình phổ biến.


Tôi đang sử dụng VC ++ ngay bây giờ ... Nhưng tôi cũng sẽ đánh giá cao bất kỳ thông tin nào liên quan đến các ngôn ngữ lập trình khác.
JNL

Ngay cả khi không có giá trị dấu phẩy động, bạn vẫn sẽ gặp phải các vấn đề tròn.
Chad

2
@Chad Đúng, nhưng mục tiêu không phải là loại bỏ các vấn đề làm tròn (sẽ luôn tồn tại, bởi vì trong bất kỳ cơ sở nào bạn sử dụng đều có một số số không có biểu diễn chính xác và bạn không có bộ nhớ và khả năng xử lý vô hạn), đó là giảm nó đến mức nó không có tác dụng trong tính toán mà bạn đang cố gắng thực hiện.
Iker

@Iker Bạn nói đúng. Mặc dù bạn, cũng không phải người đặt câu hỏi đã chỉ định chính xác những tính toán mà họ đang cố gắng đạt được và độ chính xác mà họ muốn. Anh ta cần trả lời câu hỏi đó trước khi nhảy súng vào lý thuyết số. Chỉ nói lot of mathematical calculationskhông hữu ích cũng không có câu trả lời. Trong phần lớn trường hợp (nếu bạn không giao dịch với tiền tệ) thì float thực sự đủ.
Chad

@Chad đó là một điểm công bằng, chắc chắn không có đủ dữ liệu từ OP để cho biết chính xác mức độ chính xác mà họ cần.
Iker

7

Số học dấu phẩy động thường khá chính xác (15 chữ số thập phân cho a double) và khá linh hoạt. Các vấn đề tăng lên khi bạn đang làm toán làm giảm đáng kể số lượng chữ số chính xác. Dưới đây là một số ví dụ:

  • Hủy bỏ trên phép trừ : 1234567890.12345 - 1234567890.12300, kết quả 0.0045chỉ có hai chữ số thập phân chính xác. Điều này đình công bất cứ khi nào bạn trừ hai số có độ lớn tương tự.

  • Nuốt độ chính xác: 1234567890.12345 + 0.123456789012345ước tính 1234567890.24691, mười chữ số cuối cùng của toán hạng thứ hai bị mất.

  • Phép nhân: Nếu bạn nhân hai số có 15 chữ số, kết quả có 30 chữ số cần được lưu trữ. Nhưng bạn không thể lưu trữ chúng, vì vậy 15 bit cuối cùng bị mất. Điều này đặc biệt khó chịu khi kết hợp với một sqrt()(như trong sqrt(x*x + y*y): Kết quả sẽ chỉ có 7,5 chữ số chính xác.

Đây là những cạm bẫy chính mà bạn cần phải nhận thức được. Và một khi bạn nhận thức được chúng, bạn có thể cố gắng xây dựng toán học của mình theo cách tránh chúng. Đối với bài kiểm tra, nếu bạn cần tăng giá trị nhiều lần trong một vòng lặp, hãy tránh làm điều này:

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

Sau một vài lần lặp lại, phần lớn hơn fsẽ nuốt một phần độ chính xác của df. Tồi tệ hơn, các lỗi sẽ cộng lại, dẫn đến tình huống trái ngược với việc nhỏ hơn dfcó thể dẫn đến kết quả chung tồi tệ hơn. Tốt hơn nên viết điều này:

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

Vì bạn đang kết hợp số gia trong một phép nhân, kết quả fsẽ chính xác đến 15 chữ số thập phân.

Đây chỉ là một ví dụ, có nhiều cách khác để tránh mất độ chính xác do các lý do khác. Nhưng nó giúp ích rất nhiều cho việc suy nghĩ về độ lớn của các giá trị liên quan và tưởng tượng điều gì sẽ xảy ra nếu bạn làm toán bằng bút và giấy, làm tròn đến một số chữ số cố định sau mỗi bước.


2

Làm thế nào để đảm bảo rằng bạn không gặp vấn đề: Tìm hiểu về các vấn đề số học dấu phẩy động hoặc thuê ai đó làm hoặc sử dụng một số ý nghĩa thông thường.

Vấn đề đầu tiên là độ chính xác. Trong nhiều ngôn ngữ, bạn có "float" và "double" (viết tắt của "double double") và trong nhiều trường hợp, "float" cung cấp cho bạn độ chính xác khoảng 7 chữ số, trong khi double mang lại cho bạn 15. Thông thường là nếu bạn có trong trường hợp độ chính xác có thể là một vấn đề, 15 chữ số là tốt hơn rất nhiều so với 7 chữ số. Trong nhiều tình huống có chút vấn đề, sử dụng "nhân đôi" có nghĩa là bạn thoát khỏi nó và "nổi" có nghĩa là bạn không. Giả sử vốn hóa thị trường của một công ty là 700 tỷ đô la. Đại diện cho điều này trong float, và bit thấp nhất là $ 65536. Đại diện cho nó bằng cách sử dụng gấp đôi, và bit thấp nhất là khoảng 0,012 cent. Vì vậy, trừ khi bạn thực sự, thực sự biết những gì bạn đang làm, bạn sử dụng gấp đôi, không nổi.

Vấn đề thứ hai là vấn đề nguyên tắc. Nếu bạn thực hiện hai phép tính khác nhau sẽ cho cùng một kết quả, chúng thường không xảy ra do lỗi làm tròn. Hai kết quả nên bằng nhau sẽ là "gần như bằng nhau". Nếu hai kết quả gần nhau, thì giá trị thực có thể bằng nhau. Hoặc họ có thể không. Bạn cần ghi nhớ điều đó và nên viết và sử dụng các hàm có nội dung "x chắc chắn lớn hơn y" hoặc "x chắc chắn nhỏ hơn y" hoặc "x và y có thể bằng nhau".

Vấn đề này trở nên tồi tệ hơn rất nhiều nếu bạn sử dụng làm tròn, ví dụ "làm tròn x xuống số nguyên gần nhất". Nếu bạn nhân 120 * 0,05, kết quả sẽ là 6, nhưng những gì bạn nhận được là "một số rất gần với 6". Nếu sau đó bạn "làm tròn xuống số nguyên gần nhất", thì "số rất gần với 6" có thể là "nhỏ hơn 6" và được làm tròn thành 5. Và lưu ý rằng bạn không có vấn đề chính xác đến mức nào. Không quan trọng là gần 6 kết quả của bạn như thế nào , miễn là nó nhỏ hơn 6.

Và thứ ba, một số vấn đề là khó khăn . Điều đó có nghĩa là không có quy tắc nhanh chóng và dễ dàng. Nếu trình biên dịch của bạn hỗ trợ "long double" với độ chính xác cao hơn, bạn có thể sử dụng "long double" và xem liệu nó có tạo ra sự khác biệt không. Nếu nó không có gì khác biệt, thì bạn là Ok, hoặc bạn có một vấn đề khó khăn thực sự. Nếu nó tạo ra loại khác biệt mà bạn mong đợi (như thay đổi ở số thập phân thứ 12) thì có khả năng bạn sẽ ổn. Nếu nó thực sự thay đổi kết quả của bạn, thì bạn có một vấn đề. Yêu cầu giúp đỡ.


1
Không có gì "lẽ thường" về toán học dấu phẩy động.
tên gì

Tìm hiểu thêm về nó.
gnasher729

0

Hầu hết mọi người đều mắc lỗi khi họ nhìn thấy gấp đôi họ hét BigDecimal, trong khi thực tế họ vừa chuyển vấn đề sang nơi khác. Double cho Dấu bit: 1 bit, Độ rộng số mũ: 11 bit. Độ chính xác đáng kể: 53 bit (52 được lưu trữ rõ ràng). Do tính chất của double, toàn bộ interger bạn mất độ chính xác tương đối lớn hơn. Để tính độ chính xác tương đối, chúng tôi sử dụng ở đây là dưới đây.

Độ chính xác tương đối của gấp đôi trong tính toán, chúng tôi sử dụng foluma sau 2 ^ E <= abs (X) <2 ^ (E + 1)

epsilon = 2 ^ (E-10)% Cho độ nổi 16 bit (độ chính xác một nửa)

 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

Nói cách khác, nếu bạn muốn Độ chính xác là +/- 0,5 (hoặc 2 ^ -1), kích thước tối đa mà số có thể là 2 ^ 52. Bất kỳ lớn hơn này và khoảng cách giữa các số dấu phẩy động lớn hơn 0,5.

Nếu bạn muốn độ chính xác là +/- 0,0005 (khoảng 2 ^ -11), kích thước tối đa mà số có thể là 2 ^ 42. Bất kỳ lớn hơn giá trị này và khoảng cách giữa các số dấu phẩy động lớn hơn 0,0005.

Tôi thực sự không thể đưa ra một câu trả lời tốt hơn thế này. Người dùng sẽ cần tìm ra độ chính xác mà họ muốn khi thực hiện phép tính cần thiết và giá trị đơn vị của chúng (Mét, Bàn chân, Inch, mm, cm). Đối với phần lớn các trường hợp, float sẽ đủ cho các mô phỏng đơn giản tùy thuộc vào quy mô của thế giới mà bạn đang nhắm đến để mô phỏng.

Mặc dù đó là điều cần nói, nhưng nếu bạn chỉ nhắm đến việc mô phỏng một thế giới 100 mét x 100 mét thì bạn sẽ có một nơi nào đó theo thứ tự chính xác gần 2 ^ -45. Điều này thậm chí còn không đi sâu vào việc FPU hiện đại bên trong cpu sẽ thực hiện các phép tính bên ngoài kích thước loại gốc và chỉ sau khi tính toán hoàn tất, chúng sẽ làm tròn (tùy thuộc vào chế độ làm tròn của FPU) với kích thước loại gốc.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.