Main Content

parfor を使用したモンテカルロ コードの高速化

この例では、parfor ループを使用してモンテカルロ コードを高速化する方法を説明します。モンテカルロ法は、物理学、数学、生物学、金融学など、さまざまな分野で使用されています。モンテカルロ法では、ランダムに分散された入力によって 1 つの関数が何度も実行されます。Parallel Computing Toolbox を使用すると、for ループを parfor ループに置き換えて、コードを簡単に高速化できます。

この例では、ドル オークションに基づいて単純な確率的シミュレーションを実行します。モンテカルロ法を使用し、複数のシミュレーションを実行して 1 ドル紙幣の市場価値を求めます。この例では、ドル オークションは、ランダム過程に依存する出力を生成するブラックボックス関数として扱われます。このモデルの詳細については、ドル オークションを参照してください。モンテカルロ コードを高速化するための一般的な方法を確認するには、parfor ループを使用した市場価値の推定を参照してください。

ドル オークション

ドル オークションとは、1971 年に Martin Shubik によって紹介されたノンゼロサム ゲームです。ゲームの中で、参加者は 1 ドル紙幣に入札します。1 人の参加者が入札後、他の参加者はそれぞれ前の入札者より高額での入札を選ぶことができます。入札を決断する参加者がいなくなると、オークションは終了します。最高額入札者が 1 ドル紙幣を受け取りますが、通常のオークションとは異なり、最高額の入札者と 2 番目に高い金額を提示した入札者の両方が自分の入札額をオークション主催者に渡します。

確率的モデル

確率的モデルを使用すると、ドル オークションに似たゲームをモデル化できます。状態 (現在の入札額と参加者数) は、マルコフ過程を使用してモデル化できるため、結果 (市場価値) は適切に定義された統計量をもつと予測できます。結果は条件付き分布から導き出されるため、ドル オークションはモンテカルロ解析には理想的です。市場価値は以下の要因に影響されます。

  • 参加者数 (nPlayers)

  • 参加者の行動

この例では、以下のアルゴリズムで、状態次第で参加者がどのような行動を取るか (入札するか離脱するか) を判定します。

  1. 入札額を前の入札額プラス incr に設定します。

  2. 前の入札者以外の参加者の中から、1 人の参加者をランダムに選択します。

  3. まだ入札が行われていない場合は、8 に進みます。

  4. 前の入札額が 1 未満の場合は、0 から 1 の間の乱数を生成します。この乱数が dropoutRate 未満の場合は、7 に進みます。

  5. 参加者が落札した場合に稼ぐことのできる金額 (gain) を計算します。

  6. 参加者が 2 番目に高い金額の入札者となった場合に失う金額 (loss) を計算します。gainloss より大きい場合は、8 に進みます。

  7. 参加者が離脱します。その参加者を参加者グループから削除して、9 に進みます。

  8. 参加者が入札します。

  9. 2 人以上の参加者が残っている場合は、手順 1 に進みます。

サポート関数 dollarAuction がドル オークションをシミュレートします。コードを表示するには、dollarAuction.m を参照してください。この関数は、nPlayersincr および dropoutRate の 3 つの入力を取ります。それぞれの値を設定します。

nPlayers = 20;
incr = 0.05;
dropoutRate = 0.01;

関数 dollarAuction を実行することにより、ランダムなシナリオを実行します。出力 bids および dropouts を保存します。

[bids,dropouts] = dollarAuction(nPlayers,incr,dropoutRate);

ゲームが進むにつれ、入札する参加者と離脱する参加者が出てきます。入札額が 1 を超えた場合、残り 1 名になるまで、参加者たちは "入札合戦" に陥ります。

table dropouts には、2 つの変数が含まれており、Player は各参加者に割り当てられている一意な番号、Epoch は、Player が何周目の入札時に離脱したかを示します。findgroups を使用して dropouts.Epoch をグループ化し、splitapply を使用して dropouts.Epoch のそれぞれの周回で離脱した参加者の数を取得します。

[G,epochs] = findgroups(dropouts.Epoch);
numberDropouts = splitapply(@numel,dropouts.Epoch,G);

最初は、離脱者はいません。この情報を、epochsnumberDropouts の前に 10 を付けることによって追加します。

epochs = [1;epochs];
numberDropouts = [0;numberDropouts];

nPlayerscumsum を使用して、残りの参加者数を numberDropouts から計算します。increpochs を使用して入札額を計算します。stairs を使用して、numberDropouts の累積和に対して入札額をプロットします。

playersRemaining = nPlayers - cumsum(numberDropouts);
stairs(incr*epochs,playersRemaining)
xlabel('Bid')
ylabel('Number of players remaining')

モンテカルロ法を使用した市場価値の推定

モンテカルロ法を使用することにより、紙幣の市場価値を値 origValue で推定できます。ここでは、モンテカルロ モデルを生成し、Parallel Computing Toolbox を使用する場合と使用しない場合の速度を比較します。結果をランダムにサンプリングするために使用する試行回数 nTrials を設定します。

nTrials = 10000;

サポート関数 dollarAuction を複数回実行すると、起こり得る結果をサンプリングできます。for ループを使用して nTrials 個のサンプルを生成し、各試行の最後の入札額を B に保存します。関数 dollarAuction を実行するたびに、異なる結果が得られます。しかし、関数の実行回数が多くなると、すべての実行から得られる結果は適切に定義された統計量をもちます。

nTrials 回のシミュレーションの計算が完了するまでの所要時間を記録します。経過時間の統計ノイズを削減するために、この処理を 5 回繰り返して、最短の経過時間を取ります。

t = zeros(1,5);
for j = 1:5
    tic
    B = zeros(1,nTrials);
    for i = 1:nTrials
        bids = dollarAuction(nPlayers,incr,dropoutRate);
        B(i) = bids.Bid(end);
    end
    t(j) = toc;
end
forTime = min(t)
forTime = 21.4323

histogram を使用して、最終入札額 B のヒストグラムをプロットします。xline を使用して、元の値 (1 ドル) と mean によって得られた平均市場価値をプロットに重ね合わせます。

histogram(B);
origLine = xline(1,'k','LineWidth',3);
marketLine = xline(mean(B),'k--','LineWidth',3);
xlabel('Market value')
ylabel('Frequency')
legend([origLine, marketLine],{'Original value','Market value'},'Location','NorthEast')

与えられたアルゴリズムと入力パラメーターでは、平均市場価値は元の価値より大きくなりました。

parfor ループを使用した市場価値の推定

Parallel Computing Toolbox を使用すると、モンテカルロ コードを簡単に高速化できます。まず、'local' プロファイルを使用して、4 つのワーカーをもつ並列プールを作成します。

p = parpool('local',4);
Starting parallel pool (parpool) using the 'local' profile ...
Connected to the parallel pool (number of workers: 4).

for ループを parfor ループに置き換えます。nTrials 回のシミュレーションの計算が完了するまでの所要時間を記録します。経過時間の統計ノイズを削減するために、この処理を 5 回繰り返して、最短の経過時間を取ります。

t = zeros(1,5);
for j = 1:5
    tic
    parfor i = 1:nTrials
        bids = dollarAuction(nPlayers,incr,dropoutRate);
        B(i) = bids.Bid(end);
    end
    t(j) = toc;
end
parforTime = min(t)
parforTime = 5.9174

ワーカーが 4 つの場合、parfor ループを使用するとコードを 3 倍以上高速で実行できることが結果によって示されています。

parfor ループでの乱数による再現可能な結果の生成

parfor ループで乱数を生成すると、ループを実行するたびに異なる結果が生成される可能性があります。再現可能な結果を作成するには、ループの各反復で、乱数発生器の状態が確定的でなければなりません。詳細については、parfor ループでの乱数の繰り返しを参照してください。

サポート関数 dollarAuctionStream は、4 番目の引数 s を取ります。このサポート関数は、指定されたストリームを使用して乱数を生成します。コードを表示するには、dollarAuctionStream.m を参照してください。

ストリームを作成するとき、そのストリームのサブストリームは統計的に独立しています。詳細については、RandStreamを参照してください。コードで生成される結果の分布を毎回同じにするには、ループの反復ごとに乱数発生器ストリームを作成し、Substream プロパティをループ インデックスに設定します。dollarAuctiondollarAuctionStream に置き換え、s を使用してワーカー上で dollarAuctionStream を実行します。

nTrials 回のシミュレーションの計算が完了するまでの所要時間を記録します。経過時間の統計ノイズを削減するために、この処理を 5 回繰り返して、最短の経過時間を取ります。

t = zeros(1,5);
for j = 1:5
    tic
    parfor i = 1:nTrials
        s = RandStream('Threefry');
        s.Substream = i;
        bids = dollarAuctionStream(nPlayers,incr,dropoutRate,s);
        B(i) = bids.Bid(end);
    end
    t(j) = toc;
end
parforTime = min(t)
parforTime = 8.7355

デスクトップからクラスターへのスケール アップ

デスクトップから、より多くのワーカーをもつクラスターへ、コードをスケール アップすることができます。デスクトップからクラスターへのスケール アップの詳細については、デスクトップからクラスターへのスケール アップを参照してください。

delete を使用して既存の並列プールをシャットダウンします。

delete(p);

サポート関数 dollarAuctionStreamparfor ループ内で計算します。同じ parfor ループを異なる数のワーカーで実行し、経過時間を記録します。経過時間の統計ノイズを削減するために、parfor ループを 5 回実行して、最短の経過時間を取ります。最短時間を配列 elapsedTimes に記録します。次のコードの MyCluster をクラスター プロファイルの名前に置き換えます。

workers = [1 2 4 8 16 32];
elapsedTimes = zeros(1,numel(workers));

% Create a pool using the 'MyCluster' cluster profile
p = parpool('MyCluster', 32);
Starting parallel pool (parpool) using the 'MyCluster' profile ...
Connected to the parallel pool (number of workers: 32).
for k = 1:numel(workers)    
    t = zeros(1,5);
    for j = 1:5
        tic
        parfor (i = 1:nTrials, workers(k))
            s = RandStream('Threefry');
            s.Substream = i;
            bids = dollarAuctionStream(nPlayers,incr,dropoutRate,s);
            B(i) = bids.Bid(end);
        end
        t(j) = toc;
    end
    
    elapsedTimes(k) = min(t);
end
Analyzing and transferring files to the workers ...done.

elapsedTimes(1)elapsedTimes の時間で除算して、計算の高速化を計算します。ワーカー数に対する高速化をプロットすることにより、ストロング スケーリングを調べます。

speedup = elapsedTimes(1) ./ elapsedTimes;
plot(workers,speedup)
xlabel('Number of workers')
ylabel('Computational speedup')

計算の高速化はワーカー数に伴って上昇します。