1.0, std :: generate_canonical'dan geçerli bir çıktı mı?


124

Hep rastgele sayılar, sıfır ile bir arasında yalan düşündüm olmadan1 , onlar yarı açık aralık [0,1) dan sayılardır yani. Cppreference.com üzerinde Documention ait std::generate_canonicaldoğruluyor bu.

Ancak aşağıdaki programı çalıştırdığımda:

#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;
}

Bana şu çıktıyı veriyor:

Bug!

yani bana bir mükemmel yaratıyor 1, bu da MC entegrasyonumda sorunlara neden oluyor. Bu geçerli bir davranış mı yoksa benim tarafımda bir hata mı var? Bu, G ++ 4.7.3 ile aynı çıktıyı verir.

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

ve clang 3.3

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

Bu doğru bir davranışsa, nasıl önleyebilirim 1?

Düzenleme 1 : git G ++ da aynı sorundan muzdarip görünüyor. Ben varım

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

ve ile derlemek ~/temp/prefix/bin/c++ -std=c++11 -Wl,-rpath,/home/cschwan/temp/prefix/lib64 test.c && ./a.outaynı çıktıyı lddverir

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)

Düzenleme 2 : Davranışı burada bildirdim: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=63176

Düzenleme 3 : Clang ekibi sorunun farkında görünüyor: http://llvm.org/bugs/show_bug.cgi?id=18767


21
@David Lively 1.f == 1.fher durumda (tüm durumlar nelerdir? İçinde herhangi bir değişken bile görmedim 1.f == 1.f; burada sadece bir durum var: 1.f == 1.fve bu her zaman geçerli true). Lütfen bu efsaneyi daha fazla yaymayın. Kayan nokta karşılaştırmaları her zaman kesindir.
R. Martinho Fernandes

15
@DavidLively: Hayır, değil. Karşılaştırma her zaman doğrudur. Bu, tamamen aynı olmayabilir senin işlenenler var ise onlar değişmezleri hesaplanır ve verilmez.
Orbit'te Hafiflik Yarışları

2
@Galık 1.0'ın altındaki herhangi bir pozitif sayı geçerli bir sonuçtur. 1.0 değildir. Bu kadar basit. Yuvarlama konu dışıdır: Kod rastgele bir sayı alır ve üzerinde herhangi bir yuvarlama yapmaz.
R. Martinho Fernandes

7
@DavidLively, 1.0'a eşit olan tek bir değer olduğunu söylüyor. Bu değer 1.0'dır. 1.0'a yakın değerler, 1.0 ile karşılaştırılmaz. Üretme işlevinin ne yaptığı önemli değildir: 1.0 döndürürse, 1.0 ile karşılaştırır. 1.0 döndürmezse, 1.0 ile karşılaştırılmayacaktır. Kullanan örneğiniz abs(random - 1.f) < numeric_limits<float>::epsilon, sonucun 1.0'a yakın olup olmadığını kontrol eder , ki bu bu bağlamda tamamen yanlıştır: Burada geçerli sonuçlar olan 1.0'a yakın sayılar vardır, yani 1.0'dan küçük olanların tümü.
R. Martinho Fernandes

4
@Galik Evet, bunu uygularken sorun çıkacaktır. Ancak bu sorun, uygulayıcının başa çıkması gereken bir sorundur. Kullanıcı hiçbir zaman 1.0 görmemelidir ve kullanıcı her zaman tüm sonuçların eşit dağılımını görmelidir.
R. Martinho Fernandes

Yanıtlar:


121

Sorun, std::mt19937( std::uint_fast32_t) 'nin ortak etki alanından float; standart tarafından açıklanan algoritma, mevcut IEEE754 yuvarlama modu negatife yuvarlama dışında herhangi bir şeyse (varsayılanın yuvarlak olduğunu unutmayın) hassasiyet kaybı meydana geldiğinde, standart tarafından açıklanan algoritma yanlış sonuçlar verir (algoritmanın çıktısının açıklamasıyla tutarsız) Projede öngörülen en yakın).

MT19937'nin tohumunuzla 7549723'üncü çıktısı 4294967257'dir ( 0xffffffd9u), 32 bit kayan 0x1p+32noktaya 0xffffffffuyuvarlandığında, bu da 32 bitlik kayan noktaya yuvarlandığında mt19937, 4294967295 ( ) maksimum değerine eşittir .

Belirtilmesi olsaydı standart doğru davranış sağlamak olabilir edilene URNG çıkışından dönüştürürken RealTypearasında generate_canonical, yuvarlama Negatif sonsuza gerçekleştirilecekse; bu, bu durumda doğru bir sonuç verecektir. QOI olarak libstdc ++ 'nın bu değişikliği yapması iyi olacaktır.

Bu değişiklikle 1.0artık oluşturulmayacak; yerine sınır değerleri 0x1.fffffep-Niçin 0 < N <= 8(yaklaşık olarak daha sık olarak oluşturulur 2^(8 - N - 32)başına N, MT19937 gerçek dağılımına bağlı olarak).

Ben kullanmamayı öneriyoruz floatile std::generate_canonicaldoğrudan; bunun yerine sayıyı girin doubleve ardından negatif sonsuza yuvarlayın:

    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());
    }

Bu sorun aşağıdaki durumlarda da ortaya çıkabilir std::uniform_real_distribution<float>; çözüm aynıdır, dağıtımı uzmanlaştırmak doubleve sonucu negatif sonsuza doğru yuvarlamak için float.


2
@ kullanıcı uygulama kalitesi - bir uyumlu uygulamayı diğerinden daha iyi yapan her şey, örneğin performans, uç durumlarda davranış, hata mesajlarının yararlılığı.
ecatmur

2
@supercat: Biraz ayrıntıya inmek için, sin (x) 'deki küçük hatalar sin (x) / x (ki bu x sıfıra yakın olduğunda gerçek dünya hesaplamalarında oldukça sık görülür . Π'nin katlarına yakın "ekstra kesinlik" genellikle bunun sadece bir yan etkisidir.
Ilmari Karonen

1
@IlmariKaronen: Yeterince küçük açılar için, sin (x) sadece x'tir. Java'nın sinüs fonksiyonundaki ciyaklamam, pi'nin katlarına yakın açılarla ilgilidir. Ben kodu sorar zaman,% 99 posit olur sin(x), ne gerçekten istediği (π / Math.PI) x'in sinüs olduğunu. Java'yı sürdüren insanlar, yavaş bir matematik rutini rapor etmenin Math.PI sinüsünün π ve Math.PI arasındaki fark olduğunu rapor etmenin, uygulamaların% 99'unda biraz daha az bir değer rapor etmesine rağmen daha iyi olduğu konusunda ısrar ediyorlar. daha iyi olurdu ...
supercat

3
@ecatmur Önerisi; Bunun std::uniform_real_distribution<float>bir sonucu olarak aynı sorundan muzdarip olduğunu belirtmek için bu gönderiyi güncelleyin . (Böylece uniform_real_distribution'ı arayan insanlar bu Q / A sorusuna sahip olacaklar).
MM

1
@ecatmur, neden negatif sonsuzluğa yuvarlamak istediğinizden emin değilim. Yana generate_canonicalaralığında bir sayıyı oluşturması gereken [0,1)ve biz sadece etkili gibi olacaktır sıfıra doğru yuvarlama olmaz, bazen 1,0 üreten bir hata söz ediyoruz?
Marshall Clow

39

Standarda göre 1.0geçerli değildir.

C ++ 11 §26.5.7.2 İşlev şablonu generate_canonical

Bu 26.5.7.2 bölümünde açıklanan şablondan örneklenen her bir işlev, sağlanan tek tip rasgele sayı üretecinin bir veya daha fazla çağrısının sonucunu gbelirtilen RealType'ın bir üyesine eşler, öyle ki, g i tarafından üretilen değerler geşit olarak dağıtılırsa, somutlaştırmanın sonuçları t j , 0 ≤ t j <1 , aşağıda belirtildiği gibi olabildiğince eşit olarak dağıtılır.


25
+1 OP'nin programında herhangi bir kusur göremiyorum, bu yüzden buna libstdc ++ ve libc ++ hatası diyorum ... bu da biraz olası görünmüyor, ama işte başlıyoruz.
Orbit'te Hafiflik Yarışları

-2

uniform_real_distributionİle benzer bir soruyla karşılaştım ve işte Standard'ın konuyla ilgili cimri üslubunu şu şekilde yorumluyorum:

Standart hep açısından matematik fonksiyonlarını tanımlayan matematik (Standart hala kayan nokta gibi davranıp, çünkü hiçbir zaman IEEE kayan nokta açısından, belki değil ortalama IEEE kayan nokta). Dolayısıyla, Standart'ta matematiksel ifadeler gördüğünüz her an, IEEE'den değil gerçek matematikten bahsediyor .

Standart hem söylüyor uniform_real_distribution<T>(0,1)(g)ve generate_canonical<T,1000>(g)yarı açık aralık [0,1) değerleri dönmelidir. Ancak bunlar matematiksel değerlerdir. Yarı açık aralıkta [0,1) bir gerçek sayı aldığınızda ve bunu IEEE kayan nokta olarak temsil ettiğinizde, zamanın önemli bir kısmı yuvarlanacaktır T(1.0).

Ne zaman Tolduğunu float(24 mantis bit), biz görmeyi bekliyoruz uniform_real_distribution<float>(0,1)(g) == 1.0f1 25 ^ 2 kez hakkında. Libc ++ ile kaba kuvvet denemem bu beklentiyi doğruluyor.

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); });
}

Örnek çıktı:

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

Ne zaman Tolduğunu double(53 mantis bit), biz görmeyi bekliyoruz uniform_real_distribution<double>(0,1)(g) == 1.01 54 ^ 2 kez hakkında. Bu beklentiyi test edecek sabrım yok. :)

Anladığım kadarıyla bu davranış iyi. "1.0'dan küçük" sayılar döndürdüğünü iddia eden bir dağıtımın aslında eşit olan sayıları döndürebilmesi "yarı açıklık" hissimizi rahatsız edebilir 1.0; ama bunlar "1.0" ın iki farklı anlamı, anladın mı? Birincisi matematiksel 1.0'dır; ikincisi, IEEE tek duyarlıklı kayan noktalı sayıdır 1.0. Ve onlarca yıldır tam eşitlik için kayan noktalı sayıları karşılaştırmamamız öğretildi.

Rastgele sayıları hangi algoritmaya beslerseniz verin, bazen tam olarak gerçekleşmesi umurunda olmayacaktır 1.0. Bir kayan nokta sayısıyla matematiksel işlemler dışında yapabileceğiniz hiçbir şey yoktur ve bazı matematiksel işlemler yaptığınız anda kodunuzun yuvarlama ile uğraşması gerekecektir. Eğer bile olabilir meşru olduğunu varsayalım generate_canonical<float,1000>(g) != 1.0f, sen hala varsaymak mümkün olmaz generate_canonical<float,1000>(g) + 1.0f != 2.0f, çünkü yuvarlama -. Sadece ondan uzaklaşamazsın; Öyleyse neden bu tek seferde yapabildiğinizi varsayalım?


2
Bu görüşe kesinlikle katılmıyorum. Standart yarı açık bir aralıktaki değerleri dikte ederse ve bir uygulama bu kuralı ihlal ederse, uygulama yanlıştır. Ne yazık ki, ecatmur'un cevabında doğru bir şekilde işaret ettiği gibi, standart, bir hata içeren algoritmayı da belirler. Bu ayrıca burada resmi olarak tanınır: open-std.org/jtc1/sc22/wg21/docs/lwg-active.html#2524
cschwan

@cschwan: Benim çevirim uygulama olmasıdır değildir kural kırma. Standart, [0,1) 'den değerler belirler; uygulama [0,1) 'den değerler döndürür; bu değerlerden bazıları IEEE'ye yuvarlanır, 1.0fancak bunları IEEE kayan noktalarına attığınızda bu kaçınılmazdır. Saf matematiksel sonuçlar istiyorsanız, sembolik bir hesaplama sistemi kullanın; eps1 içindeki sayıları temsil etmek için IEEE kayan noktasını kullanmaya çalışıyorsanız , günah durumundasınız demektir.
Quuxplusone

Bu hata tarafından kırılacak varsayımsal örnek: bir şeyi böle canonical - 1.0f. Her Temsil şamandıra için [0, 1.0), x-1.0fsıfırdan farklıdır. Tam olarak 1.0f ile çok küçük bir bölen yerine sıfıra bölme elde edebilirsiniz.
Peter Cordes
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.