web-dev-qa-db-ja.com

ランダムエンジンの違い

C++ 11標準では、乱数生成用にいくつかの異なるエンジンを指定しています:linear_congruential_enginemersenne_twister_enginesubtract_with_carry_engineなど。明らかに、これはstd::Randの古い使用法からの大きな変更です。

明らかに、これらのエンジンの(少なくとも一部の)主な利点の1つは、期間の長さが大幅に増加していることです(std::mt19937の名前に組み込まれています)。

ただし、エンジン間の違いはあまり明確ではありません。さまざまなエンジンの長所と短所は何ですか?どちらをもう一方に使用すべきか?一般的に推奨される賢明なデフォルトはありますか?

52
Yuushi

以下の説明から、Marsenne Twisterの方が複雑性とランダム性が高いのに対し、線形エンジンは高速ですがランダム性は低いようです。キャリー付き減算乱数エンジンは、線形エンジンの改良版であり、間違いなくよりランダムです。最後の参考文献では、メルセンヌツイスターはキャリー付き減算減算エンジンよりも複雑であると述べられています。

線形合同乱数エンジン

符号なし整数を生成する疑似乱数ジェネレータエンジン。

これは、標準ライブラリで最も単純なジェネレータエンジンです。その状態は単一の整数値であり、次の遷移アルゴリズムがあります。

x =(ax + c)mod m

ここで、xは現在の状態値、aとcはそれぞれのテンプレートパラメーター、mはそれぞれのテンプレートパラメーターで、これが0より大きい場合、またはnumerics_limits :: max()に1を加えた場合、それ以外の場合。

その生成アルゴリズムは、状態値の直接コピーです。

これにより、処理とメモリ消費の点で非常に効率的なジェネレーターになりますが、使用される特定のパラメーターに応じて、シリアル相関の程度が異なる数値が生成されます。

Linear_congruential_engineによって生成される乱数の周期はmです。 http://www.cplusplus.com/reference/random/linear_congruential_engine/

メルセンヌツイスター乱数エンジン

閉区間[0,2 ^ w-1]で符号なし整数を生成する疑似乱数ジェネレータエンジン。

このエンジンで使用されるアルゴリズムは、範囲内でほぼ均一な分布を持つ(モンテカルロ実験などで)大規模な一連の数値を計算するように最適化されています。

エンジンには、n個の整数要素の内部状態シーケンスがあり、構築時またはメンバー関数シードを呼び出すことによって生成された疑似ランダム系列で満たされます。

内部状態シーケンスはn個の要素のソースになります:状態が進むと(たとえば、新しい乱数を生成するために)、エンジンはxorマスクaを使用して現在の値をビットの組み合わせにねじることによって状態シーケンスを変更しますその値とm要素離れた値から来るパラメーターrによって決定されます(詳細については、operator()を参照してください)。

生成される乱数は、これらのねじれた値の調整されたバージョンです。テンパリングは、選択した状態値に適用されるパラメーターu、d、s、b、t、c、lによって定義される一連のシフトおよびxor操作です(operator()を参照)。

Mersenne_twister_engineによって生成される乱数には、メルセンヌ数2 ^((n-1)* w)-1と同等の周期があります。 http://www.cplusplus.com/reference/random/mersenne_twister_engine/ =

減算付きキャリー乱数エンジン

符号なし整数を生成する疑似乱数ジェネレータエンジン。

このエンジンで使用されるアルゴリズムは、遅延整数フィボナッチジェネレーターであり、r個の整数要素の状態シーケンスと1つのキャリー値を備えています。 http://www.cplusplus.com/reference/random/subtract_with_carry_engine/

加算または減算が使用される場合、遅延フィボナッチジェネレーターの最大周期は(2k-1)* ^(2M-1)です。 LFGの初期化は非常に複雑な問題です。 LFGの出力は初期条件に非常に敏感であり、特に注意を払わない限り、統計的な欠陥が最初だけでなく定期的に出力シーケンスに現れることがあります。 LFGのもう1つの潜在的な問題は、それらの背後にある数学的理論が不完全であり、理論的パフォーマンスではなく統計的検定に依存する必要があることです。 http://en.wikipedia.org/wiki/Lagged_Fibonacci_generator

そして最後に:使用するエンジンの選択にはいくつかのトレードオフが伴います。線形合同エンジンは適度に高速で、状態のストレージ要件が非常に小さいです。遅延フィボナッチジェネレーターは、高度な算術命令セットを備えていないプロセッサーでも非常に高速ですが、状態の保存が多くなり、スペクトル特性が低下する場合があります。メルセンヌツイスターは低速で、状態の保存要件が大きくなりますが、適切なパラメーターを使用すると、最も望ましいスペクトル特性を備えた最も長い非反復シーケンスが得られます(特定の望ましい定義に対して)。 in http://en.cppreference.com/w/cpp/numeric/random

27
fatihk

ポイントは、ランダムジェネレーターにはさまざまなプロパティがあり、特定の問題に対してそれらをより適切に、または適切でなくすることができるということです。

  • 期間の長さは、プロパティの1つです。
  • 乱数のqualityも重要です。
  • ジェネレーターのパフォーマンスも問題になる可能性があります。

必要に応じて、1つのジェネレーターまたは別のジェネレーターを使用できます。たとえば、高速な乱数が必要だが品質をあまり気にしない場合は、LCGが適切なオプションになる可能性があります。より良い品質の乱数が必要な場合は、Mersenne Twisterがおそらくより良いオプションです。

選択を助けるために、いくつかの標準テストと結果があります(私は間違いなく の表p.29がこれです)論文 )。


編集:紙から、

  1. LCG(論文のLCG(***))ファミリーは最速のジェネレーターですが、品質は最も劣ります。
  2. メルセンヌツイスター(_MT19937_)は少し遅いですが、より良い乱数を生成します。
  3. キャリー付き減算(SWB(***)、だと思います)は非常に低速ですが、適切に調整すると、より良いランダムプロパティを生成できます。
10
Dr_Sam

他の回答がranluxについて忘れているので、最近OpenCLに移植したAMD開発者による小さなメモがあります。

https://community.AMD.com/thread/139236

RANLUXは、「乱数」を生成する理由と、それらが優れている理由を説明する根本的な理論を持つ非常に少数の(私が実際に知っている唯一の)PRNGの1つでもあります。実際、理論が正しければ(そして私がそれを論争した人を知らないなら)、最高の贅沢レベルのRANLUXは、完全に無相関の数を最後のビットまで生成します。ピリオド(10 ^ 171)の下。他のほとんどのジェネレーターは、その品質についてほとんど何も言えません(メルセンヌツイスター、KISSなど)。)統計テストに合格する必要があります。

CERNの物理学者はこのPRNGのファンです。 '言っ途切れる。

4
rubenvb

これらの他の回答の一部の情報は、私の調査結果と矛盾します。 Visual Studio 2013を使用してWindows 8.1でテストを実行しましたが、_mersenne_twister_engine_は_linear_congruential_engine_または_subtract_with_carry_engine_よりも高品質で大幅に高速であることが一貫してわかりました。これにより、他の回答の情報を考慮に入れると、エンジンの特定の実装がパフォーマンスに大きな影響を与えると私は信じています。

これは誰にとっても驚きですが、_mersenne_twister_engine_が遅いと言われている他の回答では言及されていません。他のプラットフォームやコンパイラのテスト結果はありませんが、私の構成では、期間、品質、速度のパフォーマンスを考慮する場合、_mersenne_twister_engine_が明らかに優れています。メモリ使用量のプロファイルを作成していないため、スペース要件プロパティについて話すことができません。

テストに使用するコードは次のとおりです(移植可能にするために、windows.h QueryPerformanceXxx() AP​​I呼び出しを適切なタイミングメカニズムで置き換えるだけで済みます)。

_// compile with: cl.exe /EHsc
#include <random> 
#include <iostream>
#include <windows.h>

using namespace std;

void test_lc(const int a, const int b, const int s) {
    /*
    typedef linear_congruential_engine<unsigned int, 48271, 0, 2147483647> minstd_Rand;
    */
    minstd_Rand gen(1729);

    uniform_int_distribution<> distr(a, b);

    for (int i = 0; i < s; ++i) {
        distr(gen);
    }
}

void test_mt(const int a, const int b, const int s) {
    /*
    typedef mersenne_twister_engine<unsigned int, 32, 624, 397,
    31, 0x9908b0df,
    11, 0xffffffff,
    7, 0x9d2c5680,
    15, 0xefc60000,
    18, 1812433253> mt19937;
    */
    mt19937 gen(1729);

    uniform_int_distribution<> distr(a, b);

    for (int i = 0; i < s; ++i) {
        distr(gen);
    }
}

void test_swc(const int a, const int b, const int s) {
    /*
    typedef subtract_with_carry_engine<unsigned int, 24, 10, 24> ranlux24_base;
    */
    ranlux24_base gen(1729);

    uniform_int_distribution<> distr(a, b);

    for (int i = 0; i < s; ++i) {
        distr(gen);
    }
}

int main()
{
    int a_dist = 0;
    int b_dist = 1000;

    int samples = 100000000;

    cout << "Testing with " << samples << " samples." << endl;

    LARGE_INTEGER ElapsedTime;
    double        ElapsedSeconds = 0;

    LARGE_INTEGER Frequency;
    QueryPerformanceFrequency(&Frequency);
    double TickInterval = 1.0 / ((double) Frequency.QuadPart);

    LARGE_INTEGER StartingTime;
    LARGE_INTEGER EndingTime;
    QueryPerformanceCounter(&StartingTime);
    test_lc(a_dist, b_dist, samples);
    QueryPerformanceCounter(&EndingTime);
    ElapsedTime.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;
    ElapsedSeconds = ElapsedTime.QuadPart * TickInterval;
    cout << "linear_congruential_engine time: " << ElapsedSeconds << endl;

    QueryPerformanceCounter(&StartingTime);
    test_mt(a_dist, b_dist, samples);
    QueryPerformanceCounter(&EndingTime);
    ElapsedTime.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;
    ElapsedSeconds = ElapsedTime.QuadPart * TickInterval;
    cout << "   mersenne_twister_engine time: " << ElapsedSeconds << endl;

    QueryPerformanceCounter(&StartingTime);
    test_swc(a_dist, b_dist, samples);
    QueryPerformanceCounter(&EndingTime);
    ElapsedTime.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;
    ElapsedSeconds = ElapsedTime.QuadPart * TickInterval;
    cout << "subtract_with_carry_engine time: " << ElapsedSeconds << endl;
}
_

出力:

 100000000サンプルでのテスト。
 linear_congruential_engine時間:10.0821 
 mersenne_twister_engine時間:6.11615 
 subtract_with_carry_engine時間:9.26676 
1
markamos

私はちょうどマルノスから この答え を見て、自分でテストすることにしました。 std::chono::high_resolution_clockを使用して100000サンプルを100回測定し、平均を算出しました。私はstd::chrono::nanosecondsですべてを測定し、さまざまな結果に終わりました:

std::minstd_Randの平均値は28991658ナノ秒でした

std::mt19937の平均値は29871710ナノ秒でした

ranlux48_baseの平均値は29281677ナノ秒でした

これはWindows 7マシン上にあります。コンパイラはMingw-Builds 4.8.1 64ビットです。これは明らかにC++ 11フラグを使用しており、最適化フラグは使用していません。

-O3最適化をオンにすると、std::minstd_Randおよびranlux48_baseは、実際にhigh_precision_clockの実装が測定できる速度よりも速く実行されます。ただし、std::mt19937730045ナノ秒、つまり3/4秒かかります。

だから、彼が言ったように、それは実装固有ですが、少なくともGCCでは、平均時間は受け入れられた回答の説明が言うことに固執しているようです。 Mersenne Twisterは最適化の恩恵が最も少ないようですが、他の2つは、コンパイラーの最適化を考慮に入れると、信じられないほど高速に乱数をスローするだけです。

余談ですが、私はノイズ生成ライブラリでMersenne Twisterエンジンを使用していました(勾配を事前に計算していません)ので、速度の改善を実際に見られるように他のエンジンに切り替えます。私の場合、「真の」ランダム性は問題ではありません。

コード:

#include <iostream>
#include <chrono>
#include <random>

using namespace std;
using namespace std::chrono;

int main()
{
    minstd_Rand linearCongruentialEngine;
    mt19937 mersenneTwister;
    ranlux48_base subtractWithCarry;
    uniform_real_distribution<float> distro;

    int numSamples = 100000;
    int repeats = 100;

    long long int avgL = 0;
    long long int avgM = 0;
    long long int avgS = 0;

    cout << "results:" << endl;

    for(int j = 0; j < repeats; ++j)
    {
        cout << "start of sequence: " << j << endl;

        auto start = high_resolution_clock::now();
        for(int i = 0; i < numSamples; ++i)
            distro(linearCongruentialEngine);
        auto stop = high_resolution_clock::now();
        auto L = duration_cast<nanoseconds>(stop-start).count();
        avgL += L;
        cout << "Linear Congruential:\t" << L << endl;

        start = high_resolution_clock::now();
        for(int i = 0; i < numSamples; ++i)
            distro(mersenneTwister);
        stop = high_resolution_clock::now();
        auto M = duration_cast<nanoseconds>(stop-start).count();
        avgM += M;
        cout << "Mersenne Twister:\t" << M << endl;

        start = high_resolution_clock::now();
        for(int i = 0; i < numSamples; ++i)
            distro(subtractWithCarry);
        stop = high_resolution_clock::now();
        auto S = duration_cast<nanoseconds>(stop-start).count();
        avgS += S;
        cout << "Subtract With Carry:\t" << S << endl;
    }

    cout << setprecision(10) << "\naverage:\nLinear Congruential: " << (long double)(avgL/repeats)
    << "\nMersenne Twister: " << (long double)(avgM/repeats)
    << "\nSubtract with Carry: " << (long double)(avgS/repeats) << endl;
}
0
NeomerArcana

そのトレードオフは本当に。 A PRNG like Mersenne Twisterは、期間が非常に長く、他の優れた統計特性があるため、より優れています。

ただし、長い期間PRNG=は(内部状態を維持するために)より多くのメモリを消費し、(複雑な遷移と後処理のために)乱数の生成により多くの時間を要します。

アプリケーションのニーズに応じてPNRGを選択します。疑わしい場合はMersenne Twister、多くのツールのデフォルトです。

0
Nishanth

一般に、メルセンヌツイスターは最高の(そして最速の)RNGですが、ある程度のスペース(約2.5キロバイト)が必要です。どちらがニーズに適しているかは、ジェネレーターオブジェクトをインスタンス化する必要がある回数によって異なります。 (1回だけ、または数回インスタンス化する必要がある場合は、MTが使用するものです。数百万回インスタンス化する必要がある場合は、おそらくより小さなものです。)

一部の人々は、MTが他のいくつかよりも遅いと報告しています。私の実験によれば、これはコンパイラの最適化設定に大きく依存します。最も重要なのは-march = native設定が大きな違いを生む可能性があることです、ホストのアーキテクチャに応じて。

さまざまなジェネレーターの速度とサイズをテストする小さなプログラムを実行して、これを取得しました。

std::mt19937 (2504 bytes): 1.4714 s
std::mt19937_64 (2504 bytes): 1.50923 s
std::ranlux24 (120 bytes): 16.4865 s
std::ranlux48 (120 bytes): 57.7741 s
std::minstd_Rand (4 bytes): 1.04819 s
std::minstd_Rand0 (4 bytes): 1.33398 s
std::knuth_b (1032 bytes): 1.42746 s
0
Warp