Làm cách nào để so sánh dấu phẩy động?


84

Tôi hiện đang viết một số mã trong đó tôi có một cái gì đó dọc theo dòng:

Và sau đó ở những nơi khác, tôi có thể cần thực hiện bình đẳng:

Tóm lại, tôi có rất nhiều phép toán dấu phẩy động đang diễn ra và tôi cần thực hiện nhiều phép so sánh khác nhau cho các điều kiện. Tôi không thể chuyển đổi nó thành toán học số nguyên vì một thứ như vậy là vô nghĩa trong bối cảnh này.

Tôi đã đọc trước đây rằng so sánh dấu phẩy động có thể không đáng tin cậy, vì bạn có thể có những thứ như thế này đang diễn ra:

Tóm lại, tôi muốn biết: Làm cách nào để có thể so sánh một cách đáng tin cậy các số dấu phẩy động (nhỏ hơn, lớn hơn, bằng nhau)?

Dải số tôi đang sử dụng khoảng từ 10E-14 đến 10E6, vì vậy tôi cần làm việc với các số nhỏ cũng như lớn.

Tôi đã gắn thẻ điều này là ngôn ngữ bất khả tri vì tôi quan tâm đến cách tôi có thể thực hiện điều này cho dù tôi đang sử dụng ngôn ngữ nào.


Không có cách nào để làm điều này một cách đáng tin cậy khi sử dụng số dấu phẩy động. Sẽ luôn có những con số mà máy tính là bằng nhau mặc dù trên thực tế thì không (giả sử 1E + 100, 1E + 100 + 1), và bạn cũng sẽ thường có kết quả tính toán mà máy tính không bằng nhau mặc dù trên thực tế là như vậy (xem một trong những bình luận cho câu trả lời của nelhage). Bạn sẽ phải chọn cái nào trong hai cái mà bạn mong muốn ít hơn.
toochin

Mặt khác, giả sử bạn chỉ xử lý các số hữu tỉ, bạn có thể thực hiện một số phép tính số hữu tỉ dựa trên các số nguyên và sau đó hai số được coi là bằng nhau nếu một trong hai số có thể bị hủy thành số còn lại.
toochin

Vâng, hiện tại tôi đang làm một mô phỏng. Nơi tôi thường làm những phép so sánh này có liên quan đến các bước thời gian thay đổi (để giải một số câu đố). Có một số trường hợp mà tôi cần kiểm tra xem bước thời gian đã cho cho một đối tượng có bằng, nhỏ hơn hoặc lớn hơn bước thời gian của đối tượng khác hay không.
Mike Bailey


Tại sao không sử dụng mảng? stackoverflow.com/questions/28318610/…
Adrian P.

Câu trả lời:


69

So sánh lớn hơn / nhỏ hơn thực sự không phải là vấn đề trừ khi bạn đang làm việc ngay tại rìa của giới hạn độ chính xác float / kép.

Đối với một so sánh "mờ bằng", đây (mã Java, nên dễ dàng điều chỉnh) là những gì tôi đã nghĩ ra cho Hướng dẫn dấu chấm động sau rất nhiều công việc và tính đến nhiều lời chỉ trích:

public static boolean nearlyEqual(float a, float b, float epsilon) {
    final float absA = Math.abs(a);
    final float absB = Math.abs(b);
    final float diff = Math.abs(a - b);

    if (a == b) { // shortcut, handles infinities
        return true;
    } else if (a == 0 || b == 0 || diff < Float.MIN_NORMAL) {
        // a or b is zero or both are extremely close to it
        // relative error is less meaningful here
        return diff < (epsilon * Float.MIN_NORMAL);
    } else { // use relative error
        return diff / (absA + absB) < epsilon;
    }
}

Nó đi kèm với một bộ thử nghiệm. Bạn nên loại bỏ ngay lập tức bất kỳ giải pháp nào không thực hiện được, bởi vì nó hầu như được đảm bảo sẽ không thành công trong một số trường hợp cạnh như có một giá trị 0, hai giá trị rất nhỏ đối diện với 0 hoặc vô hạn.

Một giải pháp thay thế (xem liên kết ở trên để biết thêm chi tiết) là chuyển đổi các mẫu bit của phao thành số nguyên và chấp nhận mọi thứ trong một khoảng cách số nguyên cố định.

Trong mọi trường hợp, có lẽ không có giải pháp nào hoàn hảo cho tất cả các ứng dụng. Tốt nhất, bạn nên phát triển / điều chỉnh của riêng mình với một bộ thử nghiệm bao gồm các trường hợp sử dụng thực tế của bạn.


1
@toochin: tùy thuộc vào mức độ sai số mà bạn muốn cho phép, nhưng rõ ràng là một vấn đề khi bạn coi số không chuẩn hóa gần với số 0 nhất, dương và âm - ngoài số 0, chúng gần nhau hơn bất kỳ số nào khác các giá trị, nhưng nhiều triển khai ngây thơ dựa trên sai số tương đối sẽ coi chúng quá xa nhau.
Michael Borgwardt

2
Hừ! Bạn có một bài kiểm tra else if (a * b == 0), nhưng sau đó nhận xét của bạn trên cùng một dòng là a or b or both are zero. Nhưng không phải hai thứ này khác nhau sao? Ví dụ: nếu a == 1e-162b == 2e-162thì điều kiện a * b == 0sẽ đúng.
Mark Dickinson

1
@toochin: chủ yếu là vì mã được cho là dễ dàng di chuyển sang các ngôn ngữ khác có thể không có chức năng đó (nó cũng chỉ được thêm vào Java trong phiên bản 1.5).
Michael Borgwardt

1
Nếu chức năng đó được sử dụng nhiều (ví dụ như mọi khung hình của một trò chơi điện tử), tôi sẽ viết lại nó trong tập hợp với các tối ưu hóa hoành tráng.

1
Hướng dẫn tuyệt vời và câu trả lời tuyệt vời, đặc biệt là xem xét các abs(a-b)<epscâu trả lời ở đây. Hai câu hỏi: (1) Sẽ tốt hơn nếu đổi tất cả <s thành <=s, do đó cho phép so sánh "zero-eps", tương đương với so sánh chính xác? (2) Sẽ tốt hơn nếu sử dụng diff < epsilon * (absA + absB);thay cho diff / (absA + absB) < epsilon;(dòng cuối cùng) -?
Franz D.

41

TL; DR

  • Sử dụng hàm sau thay cho giải pháp hiện được chấp nhận để tránh một số kết quả không mong muốn trong một số trường hợp giới hạn nhất định, đồng thời có khả năng hiệu quả hơn.
  • Biết mức độ không chính xác dự kiến ​​mà bạn có trên các con số của mình và cung cấp chúng cho phù hợp trong hàm so sánh.

Đồ họa, xin vui lòng?

Khi so sánh các số dấu phẩy động, có hai "chế độ".

Chế độ đầu tiên là chế độ tương đối , trong đó sự khác biệt giữa xyđược coi là tương đối với biên độ của chúng |x| + |y|. Khi vẽ đồ thị ở dạng 2D, nó đưa ra cấu hình sau, trong đó màu xanh lá cây có nghĩa là bằng xy. (Tôi lấy epsilon0,5 cho mục đích minh họa).

nhập mô tả hình ảnh ở đây

Chế độ tương đối là chế độ được sử dụng cho các giá trị dấu chấm động "bình thường" hoặc "đủ lớn". (Nói thêm về điều đó sau).

Chế độ thứ hai là một chế độ tuyệt đối , khi chúng ta chỉ đơn giản so sánh sự khác biệt của chúng với một số cố định. Nó đưa ra cấu hình sau (một lần nữa với epsilon0,5 và relth1 để minh họa).

nhập mô tả hình ảnh ở đây

Chế độ so sánh tuyệt đối này được sử dụng cho các giá trị dấu chấm động "nhỏ".

Bây giờ câu hỏi là, làm thế nào để chúng ta kết hợp hai mẫu phản hồi đó lại với nhau.

Trong câu trả lời của Michael Borgwardt, công tắc dựa trên giá trị của diff, giá trị này sẽ nằm bên dưới relth( Float.MIN_NORMALtrong câu trả lời của anh ấy). Vùng chuyển đổi này được hiển thị như được tô trong biểu đồ bên dưới.

nhập mô tả hình ảnh ở đây

Bởi vì relth * epsilonlà nhỏ hơn relth, các bản vá lỗi màu xanh lá cây không dính vào nhau, do đó cung cấp cho các giải pháp một tài sản xấu: chúng ta có thể tìm thấy ba con số như vậy x < y_1 < y_2nhưng x == y2nhưng x != y1.

nhập mô tả hình ảnh ở đây

Lấy ví dụ nổi bật này:

Chúng tôi có x < y1 < y2, và thực tế y2 - xlà lớn hơn gấp 2000 lần y1 - x. Và với giải pháp hiện tại,

Ngược lại, trong giải pháp được đề xuất ở trên, vùng chuyển đổi dựa trên giá trị của |x| + |y|, được biểu thị bằng hình vuông có dấu gạch dưới bên dưới. Nó đảm bảo rằng cả hai khu vực kết nối một cách duyên dáng.

nhập mô tả hình ảnh ở đây

Ngoài ra, mã ở trên không có phân nhánh, có thể hiệu quả hơn. Hãy xem xét rằng các hoạt động như maxabs, mà ưu tiên cần phân nhánh, thường có hướng dẫn lắp ráp chuyên dụng. Vì lý do này, tôi nghĩ cách tiếp cận này vượt trội hơn so với một giải pháp khác là sửa lỗi của Michael nearlyEqualbằng cách thay đổi công tắc từ diff < relthsang diff < eps * relth, sau đó sẽ tạo ra mẫu phản hồi về cơ bản giống nhau.

Chuyển đổi giữa so sánh tương đối và tuyệt đối ở đâu?

Việc chuyển đổi giữa các chế độ đó được thực hiện xung quanh relth, được thực hiện như FLT_MINtrong câu trả lời được chấp nhận. Lựa chọn này có nghĩa là đại diện của float32là giới hạn độ chính xác của các số dấu phẩy động của chúng ta.

Điều này không phải lúc nào cũng có ý nghĩa. Ví dụ: nếu các số bạn so sánh là kết quả của một phép trừ, có lẽ một số thứ trong phạm vi FLT_EPSILONcó ý nghĩa hơn. Nếu chúng là căn bình phương của các số bị trừ, thì số không chính xác có thể cao hơn.

Nó là khá rõ ràng khi bạn xem xét so sánh một dấu phẩy động với 0. Ở đây, bất kỳ so sánh tương đối nào sẽ không thành công, bởi vì |x - 0| / (|x| + 0) = 1. Vì vậy, so sánh cần chuyển sang chế độ tuyệt đối khi xtheo thứ tự không chính xác trong tính toán của bạn - và hiếm khi nó thấp bằng FLT_MIN.

Đây là lý do cho sự ra đời của relththam số trên.

Ngoài ra, bằng cách không nhân relthvới epsilon, việc giải thích tham số này rất đơn giản và tương ứng với mức độ chính xác của số mà chúng ta mong đợi trên những con số đó.

Toán học ầm ầm

(giữ ở đây chủ yếu vì niềm vui của riêng tôi)

Nói chung hơn, tôi giả sử rằng một toán tử so sánh dấu phẩy động hoạt động tốt =~nên có một số thuộc tính cơ bản.

Những điều sau đây là khá rõ ràng:

  • tự bình đẳng: a =~ a
  • đối xứng: a =~ bngụ ýb =~ a
  • bất biến bởi đối lập: a =~ bngụ ý-a =~ -b

(Chúng tôi không có a =~ bb =~ cngụ ý a =~ c, =~không phải là mối quan hệ tương đương).

Tôi sẽ thêm các thuộc tính sau cụ thể hơn để so sánh dấu phẩy động

  • nếu a < b < c, thì a =~ cngụ ý a =~ b(các giá trị gần hơn cũng phải bằng nhau)
  • nếu a, b, m >= 0thì a =~ bngụ ý a + m =~ b + m(các giá trị lớn hơn với cùng sự khác biệt cũng phải bằng nhau)
  • nếu 0 <= λ < 1thì a =~ bngụ ý λa =~ λb(có lẽ ít rõ ràng hơn đối với lập luận cho).

Các thuộc tính đó đã cung cấp các ràng buộc mạnh mẽ đối với các hàm gần bằng nhau. Chức năng đề xuất ở trên xác minh chúng. Có lẽ một hoặc một số thuộc tính hiển nhiên bị thiếu.

Khi người ta nghĩ về =~một họ quan hệ bình đẳng được =~[Ɛ,t]tham số hóa bởi Ɛrelth, người ta cũng có thể thêm

  • nếu Ɛ1 < Ɛ2sau đó a =~[Ɛ1,t] bngụ ý a =~[Ɛ2,t] b(bình đẳng đối với một mức độ khoan dung nhất định có nghĩa là bình đẳng ở mức độ khoan dung cao hơn)
  • nếu t1 < t2sau đó a =~[Ɛ,t1] bngụ ý a =~[Ɛ,t2] b(bình đẳng cho một không chính xác nhất định nghĩa là bình đẳng cho một không chính xác cao hơn)

Giải pháp được đề xuất cũng xác minh những điều này.


1
Đó là một câu trả lời tuyệt vời!
davidhigh

1
Câu hỏi triển khai c ++: có (std::abs(a) + std::abs(b))thể lớn hơn bao giờ std::numeric_limits<float>::max()?
anneb

1
@anneb Có, nó có thể là + INF.
Paul Groke

16

Tôi đã gặp sự cố So sánh số dấu phẩy động A < BA > B Đây là những gì có vẻ hoạt động:

Các fabs - giá trị tuyệt đối - sẽ được quan tâm nếu chúng về cơ bản bằng nhau.


1
Không cần phải sử dụng fabsở tất cả, nếu bạn thực hiện các thử nghiệm đầu tiênif (A - B < -Epsilon)
fishinear

11

Chúng ta phải chọn một mức dung sai để so sánh các số thực. Ví dụ,

final float TOLERANCE = 0.00001;
if (Math.abs(f1 - f2) < TOLERANCE)
    Console.WriteLine("Oh yes!");

Một lưu ý. Ví dụ của bạn là khá buồn cười.

double a = 1.0 / 3.0;
double b = a + a + a;
if (a != b)
    Console.WriteLine("Oh no!");

Một số phép toán ở đây

a = 1/3
b = 1/3 + 1/3 + 1/3 = 1.

1/3 != 1

Ồ, vâng ..

Ý bạn là

if (b != 1)
    Console.WriteLine("Oh no!")

3

Tôi đã có ý tưởng để so sánh dấu phẩy động nhanh chóng

infix operator ~= {}

func ~= (a: Float, b: Float) -> Bool {
    return fabsf(a - b) < Float(FLT_EPSILON)
}

func ~= (a: CGFloat, b: CGFloat) -> Bool {
    return fabs(a - b) < CGFloat(FLT_EPSILON)
}

func ~= (a: Double, b: Double) -> Bool {
    return fabs(a - b) < Double(FLT_EPSILON)
}

1

Thích ứng với PHP từ câu trả lời của Michael Borgwardt & bosonix:

class Comparison
{
    const MIN_NORMAL = 1.17549435E-38;  //from Java Specs

    // from http://floating-point-gui.de/errors/comparison/
    public function nearlyEqual($a, $b, $epsilon = 0.000001)
    {
        $absA = abs($a);
        $absB = abs($b);
        $diff = abs($a - $b);

        if ($a == $b) {
            return true;
        } else {
            if ($a == 0 || $b == 0 || $diff < self::MIN_NORMAL) {
                return $diff < ($epsilon * self::MIN_NORMAL);
            } else {
                return $diff / ($absA + $absB) < $epsilon;
            }
        }
    }
}

1

Bạn nên tự hỏi tại sao bạn lại so sánh các con số. Nếu bạn biết mục đích của việc so sánh thì bạn cũng nên biết độ chính xác cần thiết của các con số của mình. Điều đó là khác nhau trong từng tình huống và từng ngữ cảnh ứng dụng. Nhưng trong hầu hết các trường hợp thực tế, cần có độ chính xác tuyệt đối . Rất hiếm khi áp dụng được độ chính xác tương đối.

Để đưa ra một ví dụ: nếu mục tiêu của bạn là vẽ một biểu đồ trên màn hình, thì bạn có thể muốn các giá trị dấu phẩy động so sánh bằng nhau nếu chúng ánh xạ đến cùng một pixel trên màn hình. Nếu kích thước màn hình của bạn là 1000 pixel và các con số của bạn nằm trong phạm vi 1e6, thì bạn có thể muốn 100 so sánh với 200.

Với độ chính xác tuyệt đối cần thiết, thuật toán sẽ trở thành:

public static ComparisonResult compare(float a, float b, float accuracy) 
{
    if (isnan(a) || isnan(b))   // if NaN needs to be supported
        return UNORDERED;    
    if (a == b)                 // short-cut and takes care of infinities
        return EQUAL;           
    if (abs(a-b) < accuracy)    // comparison wrt. the accuracy
        return EQUAL;
    if (a < b)                  // larger / smaller
        return SMALLER;
    else
        return LARGER;
}

0

Lời khuyên tiêu chuẩn là sử dụng một số giá trị "epsilon" nhỏ (được chọn tùy thuộc vào ứng dụng của bạn, có thể là) và coi các phao nằm trong epsilon của nhau là bằng nhau. ví dụ như một cái gì đó như

Một câu trả lời đầy đủ hơn là phức tạp, bởi vì lỗi dấu chấm động là cực kỳ tinh vi và khó hiểu để suy luận về nó. Nếu bạn thực sự quan tâm đến bình đẳng theo bất kỳ nghĩa chính xác nào, có thể bạn đang tìm kiếm một giải pháp không liên quan đến dấu phẩy động.


Điều gì sẽ xảy ra nếu anh ta đang làm việc với các số dấu phẩy động thực sự nhỏ, như 2.3E-15?
toochin

1
Tôi đang làm việc với một phạm vi khoảng [10E-14, 10E6], không hoàn toàn là epsilon máy nhưng rất gần với nó.
Mike Bailey

2
Làm việc với số lượng nhỏ không phải là vấn đề nếu bạn ghi nhớ rằng bạn phải làm việc với sai số tương đối . Nếu bạn không quan tâm dung sai lỗi về tương đối lớn, ở trên sẽ là OK nếu bạn muốn thay thế nó tình trạng này với một cái gì đó giống nhưif ((a - b) < EPSILON/a && (b - a) < EPSILON/a)
toochin

2
Đoạn mã được đưa ra ở trên cũng có vấn đề khi bạn xử lý các số rất lớn c, vì một khi số của bạn đủ lớn, EPSILON sẽ nhỏ hơn độ chính xác của máy c. Vd: giả sử c = 1E+22; d=c/3; e=d+d+d;. Sau đó, e-ccũng có thể lớn hơn đáng kể 1
toochin

1
Đối với ví dụ, hãy thử double a = pow(8,20); double b = a/7; double c = b+b+b+b+b+b+b; std::cout<<std::scientific<<a-c;(a, c không đồng đều theo pnt và nelhage), hoặc double a = pow(10,-14); double b = a/2; std::cout<<std::scientific<<a-b;(a và b bằng theo pnt và nelhage)
toochin

0

Tôi đã thử viết một hàm bình đẳng với các ý kiến ​​trên. Đây là những gì tôi nghĩ ra:

Chỉnh sửa: Thay đổi từ Math.Max ​​(a, b) thành Math.Max ​​(Math.Abs ​​(a), Math.Abs ​​(b))

Suy nghĩ? Tôi vẫn cần phải tính toán lớn hơn và nhỏ hơn.


epsilonphải là Math.abs(Math.Max(a, b)) * Double.Epsilon;, hoặc nó sẽ luôn nhỏ hơn diffâm ab. Và tôi nghĩ rằng của bạn epsilonquá nhỏ, hàm có thể không trả về bất kỳ điều gì khác với ==toán tử. Lớn hơn hiện tại a < b && !fpEqual(a,b).
toochin

1
Không thành công khi cả hai giá trị chính xác bằng 0, không thành công đối với Double.Epsilon và -Double.Epsilon, không thành công đối với số vô hạn.
Michael Borgwardt

1
Trường hợp của số vô hạn không phải là mối quan tâm trong ứng dụng cụ thể của tôi, nhưng được ghi nhận một cách hợp lý.
Mike Bailey

-1

Bạn cần lưu ý rằng lỗi cắt ngắn là một lỗi tương đối. Hai số bằng nhau nếu hiệu của chúng lớn bằng ulp của chúng (Đơn vị ở vị trí cuối cùng).

Tuy nhiên, nếu bạn thực hiện các phép tính dấu phẩy động, khả năng xảy ra lỗi của bạn sẽ tăng lên với mọi thao tác (đặc biệt là cẩn thận với các phép trừ!), Do đó khả năng chịu lỗi của bạn cần phải tăng lên tương ứng.


-1

Cách tốt nhất để so sánh các giá trị đôi về bình đẳng / bất bình đẳng là lấy giá trị tuyệt đối của sự khác biệt của chúng và so sánh với giá trị đủ nhỏ (tùy thuộc vào ngữ cảnh của bạn).

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.