Là 1.0 một đầu ra hợp lệ từ std :: created_canonical?


124

Tôi luôn nghĩ các số ngẫu nhiên sẽ nằm giữa 0 và 1, không có1 nghĩa là chúng là các số trong khoảng thời gian nửa mở [0,1). Các documention trên cppreference.com của std::generate_canonicalxác nhận điều này.

Tuy nhiên, khi tôi chạy chương trình sau:

#include <iostream>
#include <limits>
#include <random>

int main()
{
    std::mt19937 rng;

    std::seed_seq sequence{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    rng.seed(sequence);
    rng.discard(12 * 629143 + 6);

    float random = std::generate_canonical<float,
                   std::numeric_limits<float>::digits>(rng);

    if (random == 1.0f)
    {
        std::cout << "Bug!\n";
    }

    return 0;
}

Nó cho tôi đầu ra sau:

Bug!

tức là nó tạo cho tôi một sự hoàn hảo 1, điều này gây ra vấn đề trong việc tích hợp MC của tôi. Đó là hành vi hợp lệ hoặc có một lỗi về phía tôi? Điều này cho cùng một đầu ra với G ++ 4.7.3

g++ -std=c++11 test.c && ./a.out

và kêu 3,3

clang++ -stdlib=libc++ -std=c++11 test.c && ./a.out

Nếu đây là hành vi đúng, làm thế nào tôi có thể tránh 1?

Chỉnh sửa 1 : G ++ từ git dường như bị vấn đề tương tự. Tôi đang trên

commit baf369d7a57fb4d0d5897b02549c3517bb8800fd
Date:   Mon Sep 1 08:26:51 2014 +0000

và biên dịch với ~/temp/prefix/bin/c++ -std=c++11 -Wl,-rpath,/home/cschwan/temp/prefix/lib64 test.c && ./a.outcho cùng một đầu ra, lddsản lượng

linux-vdso.so.1 (0x00007fff39d0d000)
libstdc++.so.6 => /home/cschwan/temp/prefix/lib64/libstdc++.so.6 (0x00007f123d785000)
libm.so.6 => /lib64/libm.so.6 (0x000000317ea00000)
libgcc_s.so.1 => /home/cschwan/temp/prefix/lib64/libgcc_s.so.1 (0x00007f123d54e000)
libc.so.6 => /lib64/libc.so.6 (0x000000317e600000)
/lib64/ld-linux-x86-64.so.2 (0x000000317e200000)

Chỉnh sửa 2 : Tôi đã báo cáo hành vi tại đây: https://gcc.gnu.org/ormszilla/show_orms.cgi?id=63176

Chỉnh sửa 3 : Nhóm clang dường như nhận thức được vấn đề: http://llvm.org/bugs/show_orms.cgi?id=18767


21
@David Sống động 1.f == 1.ftrong mọi trường hợp (tất cả các trường hợp đều ở đó? Tôi thậm chí không thấy bất kỳ biến nào trong 1.f == 1.fđó; chỉ có một trường hợp ở đây: 1.f == 1.fvà đó là bất biến true). Xin đừng truyền bá huyền thoại này hơn nữa. So sánh điểm nổi luôn chính xác.
R. Martinho Fernandes

15
@DavidLively: Không, không phải vậy. Sự so sánh luôn chính xác. Đó là toán hạng của bạn có thể không chính xác nếu chúng được tính toán và không bằng chữ.
Các cuộc đua nhẹ nhàng trong quỹ đạo

2
@Galik bất kỳ số dương nào dưới 1.0 là kết quả hợp lệ. 1.0 thì không. Nó đơn giản như vậy. Làm tròn là không liên quan: mã nhận được một số ngẫu nhiên và không thực hiện bất kỳ làm tròn nào trên đó.
R. Martinho Fernandes

7
@DavidLively anh ấy nói rằng chỉ có một giá trị so sánh bằng 1. Giá trị đó là 1.0. Các giá trị gần bằng 1.0 không so sánh bằng 1.0. Không quan trọng chức năng tạo là gì: nếu trả về 1.0, nó sẽ so sánh bằng 1.0. Nếu nó không trả về 1.0, nó sẽ không so sánh bằng 1.0. Ví dụ của bạn sử dụng abs(random - 1.f) < numeric_limits<float>::epsilonkiểm tra nếu kết quả gần bằng 1.0 , điều này hoàn toàn sai trong ngữ cảnh này: có những số gần với 1.0 là kết quả hợp lệ ở đây, cụ thể là, tất cả những số nhỏ hơn 1.0.
R. Martinho Fernandes

4
@Galik Vâng, sẽ có rắc rối khi thực hiện điều đó. Nhưng rắc rối đó là để người thực hiện giải quyết. Người dùng không bao giờ phải thấy 1.0 và người dùng phải luôn thấy phân phối đồng đều của tất cả các kết quả.
R. Martinho Fernandes

Câu trả lời:


121

Vấn đề là trong ánh xạ từ tên miền của std::mt19937( std::uint_fast32_t) đến float; thuật toán được mô tả theo tiêu chuẩn cho kết quả không chính xác (không phù hợp với mô tả đầu ra của thuật toán) khi mất độ chính xác xảy ra nếu chế độ làm tròn của IEEE754 hiện tại là bất cứ điều gì khác ngoài vòng tròn đến vô cực (lưu ý rằng mặc định là tròn -để gần nhất).

Đầu ra 7549723 của mt19937 với hạt giống của bạn là 4294967257 ( 0xffffffd9u), khi được làm tròn thành float 32 bit 0x1p+32, bằng với giá trị tối đa của mt19937, 4294967295 ( 0xffffffffu) khi đó cũng được làm tròn thành float 32 bit.

Tiêu chuẩn này có thể đảm bảo hành vi đúng nếu nó là để xác định rằng khi chuyển đổi từ đầu ra của URNG đến RealTypecủa generate_canonical, làm tròn sẽ được thực hiện theo hướng vô cực tiêu cực; điều này sẽ cho một kết quả chính xác trong trường hợp này. Là QOI, sẽ tốt cho libstdc ++ khi thực hiện thay đổi này.

Với sự thay đổi này, 1.0sẽ không còn được tạo ra; thay vào đó, các giá trị biên 0x1.fffffep-Ncho 0 < N <= 8sẽ được tạo thường xuyên hơn (khoảng 2^(8 - N - 32)trên mỗi N, tùy thuộc vào phân phối thực tế của MT19937).

Tôi muốn giới thiệu không sử dụng floatvới std::generate_canonicaltrực tiếp; thay vì tạo số trong doublevà sau đó làm tròn tới vô cực âm:

    double rd = std::generate_canonical<double,
        std::numeric_limits<float>::digits>(rng);
    float rf = rd;
    if (rf > rd) {
      rf = std::nextafter(rf, -std::numeric_limits<float>::infinity());
    }

Vấn đề này cũng có thể xảy ra với std::uniform_real_distribution<float>; giải pháp là như nhau, để chuyên môn hóa phân phối doublevà làm tròn kết quả theo hướng vô cực âm trong float.


2
@user chất lượng thực hiện - tất cả những điều làm cho một triển khai tuân thủ tốt hơn so với hiệu suất khác, ví dụ như hiệu suất, hành vi trong các trường hợp cạnh, tính hữu ích của thông báo lỗi.
ecatmur

2
@supercat: Để lạc đề một chút, thực sự có những lý do chính đáng để cố gắng tạo các hàm sin chính xác nhất có thể cho các góc nhỏ, ví dụ vì các lỗi nhỏ trong sin (x) có thể biến thành các lỗi lớn trong sin (x) / x ( xảy ra khá thường xuyên trong các tính toán trong thế giới thực) khi x gần bằng không. "Độ chính xác thêm" gần bội số của π thường chỉ là tác dụng phụ của điều đó.
Ilmari Karonen

1
@IlmariKaronen: Đối với các góc đủ nhỏ, sin (x) chỉ đơn giản là x. Squawk của tôi tại hàm sin của Java có liên quan đến là với các góc gần bội số pi. Tôi sẽ khẳng định rằng 99% thời gian, khi mã yêu cầu sin(x), điều thực sự muốn là sin của (π / Math.PI) lần x. Những người duy trì Java khẳng định rằng tốt hơn là nên có một báo cáo thói quen toán học chậm rằng sin của Math.PI là sự khác biệt giữa π và Math.PI so với việc nó báo cáo một giá trị ít hơn một chút, mặc dù trong 99% ứng dụng sẽ tốt hơn ...
supercat

3
@ecatmur Gợi ý; cập nhật bài đăng này để đề cập rằng std::uniform_real_distribution<float>bị vấn đề tương tự như là hậu quả của việc này. (Vì vậy, những người đang tìm kiếm thống nhất_real_distribution sẽ đưa ra Q / A này).
MM

1
@ecatmur, tôi không chắc tại sao bạn muốn làm tròn về phía vô cực. Vì generate_canonicalsẽ tạo ra một số trong phạm vi [0,1)và đôi khi chúng ta đang nói về một lỗi mà nó tạo ra 1.0, việc làm tròn về 0 có hiệu quả không?
Marshall Clow

39

Theo tiêu chuẩn, 1.0là không hợp lệ.

C ++ 11 §26.5.7.2 Mẫu hàm tạo_canonical

Mỗi hàm được khởi tạo từ mẫu được mô tả trong phần 26.5.7.2 này ánh xạ kết quả của một hoặc nhiều lệnh của trình tạo số ngẫu nhiên thống nhất được cung cấp gcho một thành viên của RealType được chỉ định sao cho, nếu các giá trị g i được tạo ra gđược phân phối đồng đều, kết quả khởi tạo t j , 0 ≤ t j <1 , được phân phối đồng đều nhất có thể theo quy định dưới đây.


25
+1 Tôi không thể thấy bất kỳ lỗ hổng nào trong chương trình của OP, vì vậy tôi gọi đây là lỗi libstdc ++ và libc ++ ... điều này có vẻ hơi khó xảy ra, nhưng chúng ta sẽ đi.
Các cuộc đua nhẹ nhàng trong quỹ đạo

-2

Tôi vừa gặp một câu hỏi tương tự uniform_real_distribution, và đây là cách tôi diễn giải từ ngữ khó hiểu của Tiêu chuẩn về chủ đề:

Tiêu chuẩn luôn định nghĩa các hàm toán học theo thuật ngữ toán học , không bao giờ xét về điểm động của IEEE (vì Tiêu chuẩn vẫn giả vờ rằng dấu phẩy động có thể không có nghĩa là dấu phẩy động của IEEE). Vì vậy, bất cứ khi nào bạn thấy từ ngữ toán học trong Tiêu chuẩn, nó sẽ nói về toán học thực sự , không phải là IEEE.

Tiêu chuẩn nói rằng cả hai uniform_real_distribution<T>(0,1)(g)generate_canonical<T,1000>(g)nên trả về các giá trị trong phạm vi nửa mở [0,1). Nhưng đây là những giá trị toán học . Khi bạn lấy một số thực trong phạm vi nửa mở [0,1) và biểu thị nó dưới dạng dấu phẩy động của IEEE, thì, một phần đáng kể thời gian nó sẽ làm tròn đến T(1.0).

Khi Tfloat(24 bit mantissa), chúng tôi mong đợi để xem uniform_real_distribution<float>(0,1)(g) == 1.0fkhoảng 1 trong 2 ^ 25 lần. Thử nghiệm vũ phu của tôi với libc ++ đã xác nhận điều này.

template<class F>
void test(long long N, const F& get_a_float) {
    int count = 0;
    for (long long i = 0; i < N; ++i) {
        float f = get_a_float();
        if (f == 1.0f) {
            ++count;
        }
    }
    printf("Expected %d '1.0' results; got %d in practice\n", (int)(N >> 25), count);
}

int main() {
    std::mt19937 g(std::random_device{}());
    auto N = (1uLL << 29);
    test(N, [&g]() { return std::uniform_real_distribution<float>(0,1)(g); });
    test(N, [&g]() { return std::generate_canonical<float, 32>(g); });
}

Ví dụ đầu ra:

Expected 16 '1.0' results; got 19 in practice
Expected 16 '1.0' results; got 11 in practice

Khi Tdouble(53 bit mantissa), chúng tôi mong đợi để xem uniform_real_distribution<double>(0,1)(g) == 1.0khoảng 1 trong 2 ^ 54 lần. Tôi không đủ kiên nhẫn để kiểm tra kỳ vọng này. :)

Hiểu biết của tôi là hành vi này là tốt. Nó có thể xúc phạm đến cảm giác của chúng ta về "half-open-rangeness" rằng một phân phối tuyên bố quay trở lại con số "ít hơn 1,0" lon trong số lợi nhuận thực tế là rất bình đẳng để 1.0; Nhưng đó là hai ý nghĩa khác nhau của "1.0", thấy không? Đầu tiên là toán học 1.0; thứ hai là số dấu phẩy động chính xác đơn của IEEE 1.0. Và chúng tôi đã được dạy trong nhiều thập kỷ không so sánh các số dấu phẩy động cho sự bình đẳng chính xác.

Bất cứ thuật toán nào bạn cung cấp các số ngẫu nhiên sẽ không quan tâm nếu đôi khi nó chính xác 1.0. Bạn không thể làm gì với số dấu phẩy động ngoại trừ các phép toán và ngay khi bạn thực hiện một số phép toán, mã của bạn sẽ phải xử lý làm tròn. Ngay cả khi bạn có thể giả định một cách hợp pháp điều đó generate_canonical<float,1000>(g) != 1.0f, bạn vẫn sẽ không thể thừa nhận điều đó generate_canonical<float,1000>(g) + 1.0f != 2.0f- vì làm tròn số. Bạn không thể tránh khỏi nó; vậy tại sao chúng ta sẽ giả vờ trong trường hợp duy nhất này mà bạn có thể?


2
Tôi hoàn toàn không đồng ý với quan điểm này. Nếu tiêu chuẩn chỉ ra các giá trị từ khoảng thời gian nửa mở và việc triển khai phá vỡ quy tắc này, thì việc thực hiện là sai. Thật không may, như ecatmur đã chỉ ra chính xác trong câu trả lời của mình, tiêu chuẩn cũng chỉ ra thuật toán có lỗi. Điều này cũng được chính thức công nhận tại đây: open-std.org/jtc1/sc22/wg21/docs/lwg-active.html#2524
cschwan

@cschwan: Giải thích của tôi là việc thực hiện không vi phạm quy tắc. Tiêu chuẩn chỉ ra các giá trị từ [0,1); việc thực hiện trả về các giá trị từ [0,1); một số trong những giá trị đó xảy ra để làm tròn lên IEEE 1.0fnhưng điều đó là không thể tránh khỏi khi bạn chuyển chúng sang phao của IEEE. Nếu bạn muốn kết quả toán học thuần túy, hãy sử dụng hệ thống tính toán tượng trưng; nếu bạn đang cố gắng sử dụng dấu phẩy động của IEEE để biểu diễn các số nằm trong epskhoảng 1, bạn đang ở trong trạng thái tội lỗi.
Quuxplusone

Ví dụ giả thuyết sẽ bị phá vỡ bởi lỗi này: chia một cái gì đó cho canonical - 1.0f. Đối với mỗi float có thể biểu diễn trong [0, 1.0), x-1.0flà khác không. Với chính xác 1.0f, bạn có thể nhận được một số chia cho 0 thay vì chỉ là một ước số rất nhỏ.
Peter Cordes
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.