1.0 è un output valido di std :: generate_canonical?


124

Ho sempre pensato che i numeri casuali si trovassero tra zero e uno, senza1 , cioè sono numeri dell'intervallo semi-aperto [0,1). La documention su cppreference.com di std::generate_canonicalconferma.

Tuttavia, quando eseguo il seguente programma:

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

Mi dà il seguente output:

Bug!

cioè mi genera un perfetto 1, che causa problemi nella mia integrazione MC. È un comportamento valido o c'è un errore dalla mia parte? Questo dà lo stesso output con G ++ 4.7.3

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

e clang 3.3

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

Se questo è un comportamento corretto, come posso evitare 1?

Modifica 1 : G ++ di git sembra soffrire dello stesso problema. ci sono

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

e la compilazione con ~/temp/prefix/bin/c++ -std=c++11 -Wl,-rpath,/home/cschwan/temp/prefix/lib64 test.c && ./a.outdà lo stesso output, lddrese

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)

Modifica 2 : ho segnalato il comportamento qui: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=63176

Modifica 3 : il team di clang sembra essere a conoscenza del problema: http://llvm.org/bugs/show_bug.cgi?id=18767


21
@David Lively 1.f == 1.fin tutti i casi (quali sono tutti i casi? Non ho nemmeno visto alcuna variabile 1.f == 1.f; c'è solo un caso qui: 1.f == 1.fe questo è invariabilmente true). Per favore, non diffondere ulteriormente questo mito. I confronti in virgola mobile sono sempre esatti.
R. Martinho Fernandes,

15
@DavidLively: No, non lo è. Il confronto è sempre esatto. Sono i tuoi operandi che potrebbero non essere esatti se vengono calcolati e non letterali.
Corse di leggerezza in orbita

2
@Galik qualsiasi numero positivo inferiore a 1,0 è un risultato valido. 1.0 non lo è. E 'così semplice. L'arrotondamento è irrilevante: il codice ottiene un numero casuale e non esegue alcun arrotondamento su di esso.
R. Martinho Fernandes,

7
@DavidLively sta dicendo che esiste un solo valore che confronta uguale a 1.0. Quel valore è 1.0. I valori vicini a 1.0 non si equivalgono a 1.0. Non importa cosa fa la funzione di generazione: se restituisce 1.0 comparerà uguale a 1.0. Se non restituisce 1.0 non verrà confrontato uguale a 1.0. Il tuo esempio usando abs(random - 1.f) < numeric_limits<float>::epsiloncontrolla se il risultato è vicino a 1.0 , il che è totalmente sbagliato in questo contesto: ci sono numeri vicini a 1.0 che qui sono risultati validi, vale a dire tutti quelli che sono inferiori a 1.0.
R. Martinho Fernandes,

4
@Galik Sì, ci saranno problemi nell'implementazione. Ma quel problema è che l'implementatore deve affrontare. L'utente non deve mai vedere un 1.0 e l'utente deve sempre vedere una distribuzione uguale di tutti i risultati.
R. Martinho Fernandes,

Risposte:


121

Il problema è nel mapping dal codice di std::mt19937( std::uint_fast32_t) a float; l'algoritmo descritto dalla norma fornisce risultati errati (incompatibili con la descrizione dell'output dell'algoritmo) quando si verifica una perdita di precisione se l'attuale modalità di arrotondamento IEEE754 è diversa dall'infinito da rotondo a negativo (si noti che l'impostazione predefinita è rotonda -a-più vicino).

L'output 7549723rd di mt19937 con il seme è 4294967257 ( 0xffffffd9u), che quando arrotondato a float a 32 bit dà 0x1p+32, che è uguale al valore massimo di mt19937, 4294967295 ( 0xffffffffu) quando anche quello è arrotondato a float a 32 bit.

Lo standard potrebbe garantire un comportamento corretto se dovesse specificare che quando si converte dall'output dell'URNG al RealTypedi generate_canonical, l'arrotondamento deve essere eseguito verso l'infinito negativo; questo darebbe un risultato corretto in questo caso. Come QOI, sarebbe bene per libstdc ++ apportare questa modifica.

Con questa modifica, 1.0non verrà più generato; invece i valori limite 0x1.fffffep-Nper 0 < N <= 8verranno generati più spesso (approssimativamente 2^(8 - N - 32)per N, a seconda della distribuzione effettiva di MT19937).

Suggerirei di non usare floatcon std::generate_canonicaldirettamente; piuttosto generare il numero in doublee poi arrotondare verso l'infinito negativo:

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

Questo problema può verificarsi anche con std::uniform_real_distribution<float>; la soluzione è la stessa, specializzare la distribuzione doublee arrotondare il risultato verso l'infinito negativo in float.


2
@utente qualità dell'implementazione: tutto ciò che rende un'implementazione conforme migliore di un'altra, ad esempio prestazioni, comportamento in casi limite, utilità dei messaggi di errore.
ecatmur,

2
@supercat: Per divagare un po ', in realtà ci sono buoni motivi per provare a rendere le funzioni sinusoidali il più accurate possibile per piccoli angoli, ad es. perché piccoli errori nel peccato (x) possono trasformarsi in grandi errori nel peccato (x) / x (che si verifica abbastanza spesso nei calcoli del mondo reale) quando x è vicino a zero. La "precisione extra" vicino ai multipli di π è generalmente solo un effetto collaterale di ciò.
Ilmari Karonen,

1
@IlmariKaronen: per angoli sufficientemente piccoli, sin (x) è semplicemente x. Il mio squawk alla funzione sinusoidale di Java si riferisce a è con angoli che sono quasi multipli di pi. Direi che il 99% delle volte, quando il codice lo richiede sin(x), ciò che vuole veramente è il seno di (π / Math.PI) volte x. Le persone che mantengono Java insistono sul fatto che è meglio avere un rapporto di routine matematica lento che il seno di Math.PI è la differenza tra π e Math.PI piuttosto che riportare un valore leggermente inferiore, nonostante che nel 99% delle applicazioni esso sarebbe meglio ...
supercat

3
@ecatmur Suggerimento; aggiorna questo post per menzionare che std::uniform_real_distribution<float>soffre dello stesso problema in conseguenza di ciò. (In modo che le persone alla ricerca di uniform_real_distribution abbiano questo Q / A emergere).
MM

1
@ecatmur, non sono sicuro del motivo per cui vuoi arrotondare all'infinito negativo. Dal momento che generate_canonicaldovrebbe generare un numero nell'intervallo [0,1)e stiamo parlando di un errore in cui genera 1.0 di tanto in tanto, l'arrotondamento verso lo zero non sarebbe altrettanto efficace?
Marshall Clow,

39

Secondo lo standard, 1.0non è valido.

C ++ 11 §26.5.7.2 Modello di funzione generate_canonical

Ciascuna funzione istanziata dal modello descritto in questa sezione 26.5.7.2 associa il risultato di una o più chiamate di un generatore di numeri casuali uniforme fornita gad un membro della RealType specificato in modo tale che, se i valori g i Per prodotto gsono uniformemente distribuiti, la i risultati dell'istanza t j , 0 ≤ t j <1 , sono distribuiti nel modo più uniforme possibile come specificato di seguito.


25
+1 Non riesco a vedere alcun difetto nel programma dell'OP, quindi lo chiamo un bug libstdc ++ e libc ++ ... che a sua volta sembra un po 'improbabile, ma ci siamo.
Razze di leggerezza in orbita

-2

Ho appena incontrato una domanda simile con uniform_real_distribution, ed ecco come interpreto la formulazione parsimoniosa dello Standard sull'argomento:

Lo standard definisce sempre le funzioni matematiche in termini matematici , mai in termini di virgola mobile IEEE (poiché lo standard fa ancora finta che il virgola mobile potrebbe non significare il virgola mobile IEEE). Quindi, ogni volta che vedi una formulazione matematica nello Standard, parla di vera matematica , non di IEEE.

Lo standard afferma che entrambi uniform_real_distribution<T>(0,1)(g)e generate_canonical<T,1000>(g)dovrebbero restituire valori nell'intervallo semi-aperto [0,1). Ma questi sono valori matematici . Quando prendi un numero reale nell'intervallo semi-aperto [0,1) e lo rappresenti come punto in virgola mobile IEEE, beh, una frazione significativa del tempo che arrotonderà per eccesso T(1.0).

Quando Tè float(24 bit di mantissa), ci aspettiamo di vedere uniform_real_distribution<float>(0,1)(g) == 1.0fcirca 1 in 2 ^ 25 volte. La mia sperimentazione a forza bruta con libc ++ conferma questa aspettativa.

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

Esempio di output:

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

Quando Tè double(53 bit di mantissa), ci aspettiamo di vedere uniform_real_distribution<double>(0,1)(g) == 1.0circa 1 in 2 ^ 54 volte. Non ho la pazienza di testare questa aspettativa. :)

La mia comprensione è che questo comportamento va bene. Essa può offendere il senso di "half-open-rangeness" che una distribuzione pretesa di restituire numeri "minore di 1.0" lattina in numero di ritorno fatto che sono uguali a 1.0; ma quelli sono due significati diversi di "1.0", vedi? Il primo è il matematico 1.0; il secondo è il numero IEEE a virgola mobile a precisione singola 1.0. E ci è stato insegnato per decenni a non confrontare i numeri in virgola mobile per l'uguaglianza esatta.

Qualunque algoritmo in cui dai i numeri casuali non ti importerà se a volte arriva esattamente 1.0. Non c'è niente che tu possa fare con un numero in virgola mobile tranne le operazioni matematiche e non appena eseguirai alcune operazioni matematiche, il tuo codice dovrà occuparsi dell'arrotondamento. Anche se si potrebbe legittimamente supporre che generate_canonical<float,1000>(g) != 1.0f, è ancora non sarebbe in grado di assumere che generate_canonical<float,1000>(g) + 1.0f != 2.0f- a causa dell'arrotondamento. Non puoi scappare da esso; quindi perché dovremmo fingere in questo singolo caso che puoi?


2
Non sono assolutamente d'accordo con questa opinione. Se lo standard determina i valori da un intervallo semiaperto e un'implementazione infrange questa regola, l'implementazione è errata. Sfortunatamente, come correttamente sottolineato da ecatmur nella sua risposta, lo standard determina anche l'algoritmo che ha un bug. Anche questo è ufficialmente riconosciuto qui: open-std.org/jtc1/sc22/wg21/docs/lwg-active.html#2524
cschwan

@cschwan: La mia interpretazione è che l'implementazione non infrange la regola. Lo standard detta valori da [0,1); l'implementazione restituisce valori da [0,1); alcuni di questi valori capita di arrotondare a IEEE 1.0fma è inevitabile quando li lanci su float IEEE. Se vuoi risultati matematici puri, usa un sistema di calcolo simbolico; se si sta tentando di utilizzare IEEE in virgola mobile per rappresentare numeri compresi in eps1, ci si trova in uno stato di peccato.
Quuxplusone,

Esempio ipotetico che verrebbe interrotto da questo errore: dividere qualcosa per canonical - 1.0f. Per ogni float rappresentabile [0, 1.0), x-1.0fè diverso da zero. Con esattamente 1.0f, puoi ottenere un divisore per zero invece di un divisore molto piccolo.
Peter Cordes,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.