1.0はstd :: generate_canonicalからの有効な出力ですか?


124

私はいつも、乱数は0と1の間にあり、なし1であると考えていました。つまり、乱数は半開区間[0,1)からの数値です。これはcppreference.comドキュメントstd::generate_canonical確認できます。

ただし、次のプログラムを実行すると、

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

それは私に次の出力を与えます:

Bug!

つまり1、MC統合で問題を引き起こす完璧なを生成します。それは有効な動作ですか、それとも私の側にエラーがありますか?これにより、G ++ 4.7.3でも同じ出力が得られます

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

およびclang 3.3

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

これが正しい動作である場合、どうすれば回避でき1ますか?

編集1:gitのG ++でも同じ問題が発生するようです。私はついています

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

でコンパイルすると~/temp/prefix/bin/c++ -std=c++11 -Wl,-rpath,/home/cschwan/temp/prefix/lib64 test.c && ./a.out同じ出力がldd得られます

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)

編集2:私はここで動作を報告しました:https//gcc.gnu.org/bugzilla/show_bug.cgi?id = 63176

編集3:clangチームは問題を認識しているようです:http : //llvm.org/bugs/show_bug.cgi?id=18767


21
@David Lively 1.f == 1.fすべてのケースで(すべてのケースは何ですか?に変数が表示されていません1.f == 1.f。ここには1つのケースしかありません。1.f == 1.fそれは常にですtrue)。この神話をこれ以上広めないでください。浮動小数点の比較は常に正確です。
R.マルティーニョフェルナンデス

15
@DavidLively:いいえ、違います。比較は常に正確です。それらが計算され、リテラルではない場合、正確でない可能性があるのはオペランドです。
オービットのライトネスレース

2
@Galik 1.0未満の正の数値は有効な結果です。1.0ではありません。それはそれと同じくらい簡単です。丸めは無関係です。コードは乱数を取得し、それに対して丸めを実行しません。
R.マルティーニョフェルナンデス

7
@DavidLively彼は1.0に等しい値が1つだけあると言っています。その値は1.0です。1.0に近い値は、1.0と同等ではありません。生成関数が何をするかは問題ではありません。1.0を返す場合、1.0と比較されます。1.0を返さない場合は、1.0と比較されません。この使用例でabs(random - 1.f) < numeric_limits<float>::epsilonは、結果が1.0近いかどうかを確認しますが、これはこのコンテキストでは完全に間違っています。1.0に近い数値、つまり、1.0未満のすべての数値がここで有効です。
R.マルティーニョフェルナンデス

4
@Galikはい、実装に問題が発生します。しかし、その問題は実装者が対処することです。ユーザーは1.0を決して表示してはならず、すべての結果が常に均等に分布している必要があります。
R.マルティーニョフェルナンデス

回答:


121

std::mt19937std::uint_fast32_t)のコドメインからへのマッピングに問題がありfloatます。標準で記述されているアルゴリズムは、現在のIEEE754丸めモードが丸めから負の無限大以外の場合に精度が失われると、誤った結果(アルゴリズムの出力の記述と一致しない)を返します(デフォルトは丸めであることに注意してください) -to-nearest)。

シードを含むmt19937の7549723番目の出力は、4294967257(0xffffffd9u)です。これは0x1p+32、32ビットfloatに丸めた場合にを生成します。これは、mt19937の最大値に等しく、これ0xffffffffuも32ビットfloatに丸めた場合は4294967295()です。

URNGの出力からに変換するときRealTypegenerate_canonical、丸めを負の無限大に向けて実行するように指定すると、標準は正しい動作を保証できます。この場合、正しい結果が得られます。QOIとして、libstdc ++がこの変更を行うとよいでしょう。

この変更により、1.0は生成されなくなります。代わりに、境界値0x1.fffffep-Nのためには、0 < N <= 8(およそより頻繁に生成される2^(8 - N - 32)ごとにN、MT19937の実際の分布に応じて)。

floatstd::generate_canonical直接使用しないことをお勧めします。むしろ、数を生成し、double負の無限大に向かって丸めます。

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

この問題は、でも発生する可能性がありますstd::uniform_real_distribution<float>。解は同じです。分布を特殊化しdouble、結果をの負の無限大に向かって丸めfloatます。


2
@user実装の品質-パフォーマンス、エッジケースでの動作、エラーメッセージの有用性など、1つの適合実装を別の実装よりも優れているものすべて。
ecatmur 2014

2
@supercat:少し脱線するには、sin(x)の小さなエラーがsin(x)/ xの大きなエラーに変わる可能性があるため、小さな角度で正弦関数をできるだけ正確にしようとする正当な理由がありますxがゼロに近い場合、現実の計算で頻繁発生します。πの倍数に近い "超精度"は、一般にその副作用にすぎません。
Ilmari Karonen 14

1
@IlmariKaronen:十分に小さい角度の場合、sin(x)は単にxです。Javaの正弦関数に関する私の不法行為は、piの倍数に近い角度で発生します。99%の確率でsin(x)、コードがを要求する場合、それが本当に必要とするのは(π/ Math.PI)倍xのサインです。Javaを保守している人々は、Math.PIの正弦はπとMath.PIの差であると報告するほうが、アプリケーションの99%でそれにもかかわらずより良いでしょう...
スーパーキャット

3
@ecatmur提案; このstd::uniform_real_distribution<float>結果として同じ問題に苦しんでいることを言及するためにこの投稿を更新してください。(したがって、uniform_real_distributionを検索する人がこのQ / Aを出すようにします)。
MM

1
@ecatmur、なぜ負の無限大に丸めたいのかわかりません。以来は、generate_canonical範囲内の番号を生成する必要があります[0,1)し、我々はそれだけで有効なものとしてもゼロに向かって丸めないと、時折1.0を生成し、エラーの話をしていますか?
Marshall Clow、2015

39

規格によると、1.0無効です。

C ++ 11§26.5.7.2関数テンプレートgenerate_canonical

このセクション26.5.7.2で説明テンプレートからインスタンス化された各機能は、供給された一様乱数発生器の一つ以上の呼び出しの結果をマッピングg指定RealTypeの一方のメンバーになるように、値G場合iはによって生成さがg均一に分布しています、インスタンス化の結果t j0≤t j <1は、以下に指定するように、可能な限り均一に分散されます。


25
+1 OPのプログラムに欠陥が見当たらないため、これをlibstdc ++およびlibc ++のバグと呼んでいます...それ自体は少しありそうにありませんが、それで完了です。
オービットのライトネスレース

-2

私はで同様の質問に遭遇しました。これがuniform_real_distribution、この主題に関する標準の節減的な表現をどのように解釈するかです。

標準は常にの面で数学関数を定義していない数学(標準はまだ浮動小数点ことをふりをするのでIEEE浮動小数点の面で、決してないかもしれません IEEE浮動小数点を意味します)。したがって、標準で数学の表現を見るときはいつでも、それはIEEEではなく、実際の数学について語っています。

標準では、両方のことを言うuniform_real_distribution<T>(0,1)(g)generate_canonical<T,1000>(g)半開範囲[0,1)の値を返す必要があります。しかし、これらは数学的な値です。半分開いた範囲[0,1)の実数を取り、それをIEEE浮動小数点として表す場合、まあ、時間のかなりの部分が切り上げられてになりT(1.0)ます。

ときTであるfloat(24仮数ビット)、私たちが見ることを期待するuniform_real_distribution<float>(0,1)(g) == 1.0f2 ^ 25回で約1。libc ++を使った力ずくの実験により、この期待が裏付けられました。

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

出力例:

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

ときTであるdouble(53仮数ビット)、私たちが見ることを期待するuniform_real_distribution<double>(0,1)(g) == 1.02 ^ 54回に1程度。私はこの期待を試す忍耐力がありません。:)

私の理解では、この動作は問題ありません。これは、ディストリビューションは、「1.0未満」であり、実際の復帰数の缶の番号を返すように主張していることを「半オープンrangeness」の私達の感覚を怒らせることに等しい1.0、しかし、これらは「1.0」の2つの異なる意味です、参照してください?1つ目は数学的 1.0です。2番目はIEEE単精度浮動小数点数1.0です。そして、私たちは何十年もの間、浮動小数点数を正確に等しいかどうか比較しないように教えてきました。

乱数を入力するアルゴリズムがどんなものであっても、正確に取得できる場合は問題になりません1.0。数学演算を除いて、浮動小数点数でできること何もありません。数学演算を行うとすぐに、コードは丸めを処理する必要があります。あなたがいても可能性が合法的にそれを想定してgenerate_canonical<float,1000>(g) != 1.0f、あなたはまだそれを想定することはできませんgenerate_canonical<float,1000>(g) + 1.0f != 2.0fので、丸めの- 。あなたはそれから逃れることはできません。では、なぜこの単一のインスタンスであなたができるふりをするのでしょうか。


2
私はこの見解に強く反対します。標準がハーフオープン間隔からの値を指示し、実装がこのルールに違反する場合、実装は間違っています。残念ながら、ecatmurが彼の回答で正しく指摘しているように、標準にはバグのあるアルゴリズムも指定されています。これは、ここでも正式に認識されています:open-std.org/jtc1/sc22/wg21/docs/lwg-active.html#2524
cschwan '

@cschwan:私の解釈では、実装規則に違反していません。標準は[0,1]からの値を指示します。実装は[0,1]から値を返します。これらの値のいくつかはたまたまIEEEに切り上げられ1.0fますが、IEEE floatにキャストする場合、それは避けられません。純粋な数学的結果が必要な場合は、記号計算システムを使用してください。IEEE浮動小数点を使用してeps1 以内の数値を表現しようとしている場合は、罪の状態にあります。
Quuxplusone 2017

このバグによって壊れる仮説の例:で何かを除算しcanonical - 1.0fます。で表現可能なすべてのfloatについて[0, 1.0)x-1.0fはゼロ以外です。正確に1.0fを使用すると、非常に小さな除数の代わりにゼロ除算を取得できます。
Peter Cordes
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.