2. "Implementing QuantLib"の和訳
Chapter-VI The Monte Carlo Framework
6.1 Pathの生成 (つづき)
6.1.3 Random Path Generator : Pathの生成装置
確率変数の“Pathの生成”を正面から取扱う前に、最後の基本的なパーツの説明が必要です。すなわち、Pathを保持する構造です。下記 Listing 6.9に、Pathクラスの定義を示しますが、このクラスは1変数のランダムな Pathをモデル化したものです。このクラスは、TimeGridクラスのインスタンスと、各時間における変数の値を持つ Arrayインスタンスを保持しています。加えて、これらの値に順次アクセスしたり Iterateするメソッド群を提供しています。各メソッドの名前は、標準的なコンテナクラスが持つメソッド名に準拠しており、より馴染みのあるインターフェースを提供しています。time( )メソッドのうように、ドメインをベースにしたメソッド名も提供されています。
Listing 6.9: Interface of the Path and MultiPath classes.
class Path {
public:
Path(const TimeGrid& timeGrid, const Array& values);
bool empty() const;
Size length() const;
Real operator[](Size i) const;
Real at(Size i) const;
Real& operator[](Size i);
Real& at(Size i);
Real front() const;
Real& front();
Real back() const;
Real& back();
Real value(Size i) const;
Real& value(Size i);
Time time(Size i) const;
const TimeGrid& timeGrid() const;
typedef Array::const_iterator iterator;
typedef Array::const_reverse_iterator reverse_iterator;
iterator begin() const;
iterator end() const;
reverse_iterator rbegin() const;
reverse_iterator rend() const;
private:
TimeGrid timeGrid_;
Array values_;
};
class MultiPath {
public:
MultiPath(const std::vector<Path>& multiPath);
// ...more constructors...
Size assetNumber() const;
Size pathSize() const;
const Path& operator[](Size j) const;
const Path& at(Size j) const;
Path& operator[](Size j);
Path& at(Size j);
private:
std::vector<Path> multiPath_;
};
MultiPathクラスも、この Listingの中で示していますが、複数の確率変数の Pathを保持しています。このクラスは基本的には装飾されたコンテナクラスで、Pathインスタンスの配列を保持し、(オブジェクト指向プログラミングにおける)デメテルの法則に則った追加のインスペクター関数を提供しています。(訳注:オブジェクト指向言語におけるデメテルの法則。自分が知っているオブジェクト(自分自身あるいは、引数として取るオブジェクト)とだけCommunicationし、それらが持つデータだけを使うべきであり、他のオブジェクト経由で、さらに第3者的なオブジェクトの内容にアクセスすべきでないし、使うべきでもない。参考 Wiki https://ja.wikipedia.org/wiki/デメテルの法則 )
(注: 例えば、MultiPathクラスのインスタンスをpとすると、追加で提供されるメソッドを使ってp.pathSize( )のような呼び出しができるようします。そうでないと、(各PathインスタンスのメソッドをMultiPathのインスタンスから呼び出す形で)p[0].length( )のような関数呼出しになります。こうすると、コードとしても醜い上に、p[1].length( )とすると、返ってくる値が異なる可能性があります。(訳注:MultiPathクラスが持つPathの配列の個別のインスタンスを経由して、Pathオブジェクトのメソッドを呼び出すと、デメテルの法則に反する))
実装方法をシンプルにするため、メモリースペースを若干犠牲にしています。Pathインスタンスの配列を保持することにより、N個の全く同じ TimeGridインスタンスのコピーを保持することになっています。
< Aside : アクセスパターン >
Pathクラスが提供している基本的なインスペクター関数は (例えば operator[ ]や iteratorのインターフェース)、単に確率変数の値 \(S_i\) だけを返し、TimeGrid上の時間も含めた \( (t_i,S_i)\) というペアの値を返すようにはしていません。p[i] がその Nodeの情報をすべて返すと期待されるのは当然ですが、ほとんどの場合、ユーザーはそのNodeでの'値'しか必要としないでしょう。従って、そのようなアクセスメソッドを最適化しました。(注: これは、Huffmanのプログラムコード原則に従っています。その原則によれば、最も汎用的な動作は最も少ない文字数で書かれるべきという事です。Perl言語が輝いているのはそのせいだと教えられました)
もしNodeの情報全体を返すようなメソッドを Pathクラスに加えるとすれば、そのデータの受け皿として std::pair<Time , Real> といった型を使わず、内部クラスとして Node構造体を使った方がいいでしょう。これは、我々が直感的に最も複雑な実装方法を使いたいと考えるからではなく (時々そういう事もありますが)、例えば p.node(i).second といったアクセス方法が解りにくいと感じるからです (このsecondは何を意味するのか?時間それとも値?)。これを p.node(i).valueとする方が、意味が解りやすく、Node構造体を定義する手間に見合うと考えるからです。
***********************************************************************************
Pathを生成する為にやるべき事で残っているのは、これまでに説明してきた各クラスを単純に連携させる事です。そのロジックは、MultiPathGeneratorクラステンプレートに実装されていますが、その内容を下記 Listing 6.10に示します。このクラスが Factoryパターンの使用例になっているかどうか、読者の方の判断にお任せします。
Listing 6.10: Sketch of the MultiPathGenerator class template.
template <class GSG>
class MultiPathGenerator {
public:
typedef Sample<MultiPath> sample_type;
MultiPathGenerator(const shared_ptr<StochasticProcess>&,
const TimeGrid&,
GSG generator,
bool brownianBridge = false);
const sample_type& next() const { return next(false); }
const sample_type& antithetic() const { return next(true); }
private:
const sample_type& next(bool antithetic) const;
bool brownianBridge_;
shared_ptr<StochasticProcess> process_;
GSG generator_;
mutable sample_type next_;
};
template <class GSG>
const typename MultiPathGenerator<GSG>::sample_type&
MultiPathGenerator<GSG>::next(bool antithetic) const {
if (brownianBridge_) {
QL_FAIL("Brownian bridge not supported");
} else {
typedef typename GSG::sample_type sequence_type;
const sequence_type& sequence_ =
antithetic ? generator_.lastSequence()
: generator_.nextSequence();
Size m = process_->size(), n = process_->factors();
MultiPath& path = next_.value;
Array asset = process_->initialValues();
for (Size j=0; j<m; j++)
path[j].front() = asset[j];
Array temp(n);
next_.weight = sequence_.weight;
TimeGrid timeGrid = path[0].timeGrid();
Time t, dt;
for (Size i = 1; i < path.pathSize(); i++) {
Size offset = (i-1)*n;
t = timeGrid[i-1];
dt = timeGrid.dt(i-1);
if (antithetic)
std::transform(sequence_.value.begin()+offset,
sequence_.value.begin()+offset+n,
temp.begin(),
std::negate<Real>());
else
std::copy(sequence_.value.begin()+offset,
sequence_.value.begin()+offset+n,
temp.begin());
asset = process_->evolve(t, asset, dt, temp);
for (Size j=0; j<m; j++)
path[j][i] = asset[j];
}
return next_;
}
}
コンストラクターは、引数として以下の変数を取り、メンバー変数に格納します。
―StochasticProcessインスタンス。
―Pathの離散時間の各ノードを生成するTimeGridインスタンス
―ガウス分布する乱数列の生成装置(一度の動作で、確率変数の数Nと時間ステップの数Mに対応する数 (N x M) だけ乱数を生成する)
―Brownian Bridgeを使うかどうかの Bool変数。但し、現時点では、古いバージョンとの互換性の問題から対応できていないので、trueに設定できません。
このクラスのインターフェースとして next( )メソッドが提供されており、新しい確率変数の Pathを返します (すみませんMultiPathです。やや不正確な点ご容赦)。また、antithetic( )メソッドは、直前の next( )メソッドで生成された Pathの 対称Path を生成します (訳注:Antithetic法はモンテカルロシミュレーションの収束速度をアップさせる為のテクニック。適切な邦訳がみつからなかったがあえてつけるのであれば対称変量生成法?)。(antithetic( )法の正確性はこのメソッドが呼び出されるタイミングに依存します。すなわち、next( )メソッドの直後になるべきです。しかし、残念ながら、これを実現する受容可能な方法を思いつきませんでした。)
この両方のメソッドとも、実装内容はprivateメソッドであるnext(bool)を呼び出しているだけで、そのnext(bool)の中で実際のPath生成の仕組みが実装されています。まず brownianBridgeのフラッグをチェックし、trueに設定されていた場合は例外処理に飛びます (現時点ではBrownian Bridgeには対応できていないため)。
(訳注: Brownian Bridgeとは、ある2つの時点t1、t2での確率変数の値が判っていた場合、そのt1、t2の間の時間t ( t1 < t < t2) における確率変数の分布を示す式。)
C++の基本原則に則れば、例外処理は出来るだけプロセスの早い段階で発生させるべきであり、従って、この例外処理はコンストラクターに移すべきでしょう (もっと良いのは、Brownian Bridgeに対応させる事ですが、もしその方法があれば、是非教えて頂きたいものです)。今のままだと、コンストラクターがうまく作動して Pathインスタンスを生成に成功したとしても、後でそのインスタンスが使えないという事になりかねません。
もし Brownian Bridgeを使わない設定なら、この nextメソッドの本体が作動し始めます。最初に、必要な乱数列を取り出します。新しい Pathを要求している場合は、新しい乱数列を、antitheticの Pathを要求している場合は、直近に生成された乱数列を持ってきます。次に、いくつかの設定を行います。まず生成される Pathの参照を取ってきて(それはメンバー変数の参照であり、一時的なローカル変数ではありません。これについては後で詳しく説明します)、次に各確率変数の初期値 (t0の値) をProcessインスタンスから取りだして、その Pathに初期値としてコピーし、さらに TimeGridの各ノードにおける確率変動成分の値を一時的に保持する為の配列を生成し、そしてさらに Pathから TimeGridの情報を取り出します。 最後に、それらの部品をひとつに纏めます。まず Pathの各ステップのスタート値としてひとつ前のステップの値を取ります (注: 初期値を入れた配列と同じ配列を使うので、一番最初のステップではすでに入っている初期値がスタート値になります)。次にTimeGridから時間 t と時間間隔 dt を取り出し、それが、確率変数が変動していく時間間隔になります。そして必要な数の (n 個の) 確率変動成分の集合をガウス乱数列のインスタンスから取ってきます (一番最初のステップでは、最初の n 個、次のステップではその次の n 個と順番に取っていきます。n は確率変動成分の数です。antithetic変量を使う場合は、直前の n 個の乱数の正負を逆転させて使います)。そこから Processインスタンスの evolve( )メソッドを呼び出し、計算結果 (訳注: そのステップにおける確率変数のシミュレーション値=ドリフト項+拡散項)を各 Pathのステップ毎の保存場所にコピーされます。
既に述べたように、返し値となる Pathは MultiPathGeneratorインスタンスのメンバー変数として保持されます (訳注:上記プログラムコード中のnext_。型タイプはSample
< Aside : 自分のつま先を踏んでしまう
残念ながら、MultiPathの生成過程において、かなりの量のメモリー確保のプロセスが行われます。その典型的な例 (現ステップの確率変数の値と、現ステップの確率変動成分の集合を保持するため、2種類のArrayインスタンスを生成する必要がある)については、データをメンバー変数に保持する事によって避けられるかも知れません。しかしそうすると、より多くのmutableなメンバー変数を持たないといけませんが、それは出来るだけ避けるべきです。また2つの配列用のメモリー確保はPath生成プロセス全体で一回しか起こりませんので、それ程大きな改善にはなりません。
それではなく、最も問題を起こしているのは原資産の StochasticProcessインスタンスで、各 Time Step毎に、evolve( )からの戻り値の為に、Arrayインスタンスを生成しています。これをどうしたら修正できるでしょうか? この場合、メンバー変数に持たせるという解決策は出来るなら避けたいです。StochasticProcessクラスは、マルチスレッド環境でもうまく動作するように出来ており (もちろん、計算中に市場データが動かないか、少なくとも市場データをその間固定するという前提ですが)、その方向性を壊したくありません。別の解決策としては、evolve( )メソッドが計算結果の戻り値を保持する為の配列を、引数として取ってくる方法です。しかし、その場合、私のみならずコード開発者はみんな、変数の命名を非常に慎重に行なわなければなりません。悲しい事実として、もしevolve( )メソッドを、そのようにプログラムしようとすると、次のようなプログラムコードになるでしょう。(確率変数が例えば2個と想定した場合の模擬的なコードです。)
void evolve(const Array& x, Time t, Time dt,
const Array& w, Array& y) {
y[0] = f(x[0], x[1], t, dt, w[0], w[1]);
y[1] = g(x[0], x[1], t, dt, w[0], w[1]);
}
ここで、x は入力データ用の配列で y は出力データ用の配列になります。ところが、ユーザーの方がこのメソッドを利用する為にプログラムコードを書くとすると、次のように書くことになるでしょう。
p->evolve(x,t,dt,w,x);
(すなわち、同じ変数 x を入力データ用と出力データ用に使って) 変数に保持されている古いデータを、計算後の新しいデータに入れ替えようとしています。
さて、問題に気付かれたでしょう。evolve( )の中の最初の関数は (コード中の f() )、y[0]にデータを書き込んでいますが、実際には x[0] に書き込んでいる事になります。従って、次の関数は (コード中の g( ) )、x[0]を引数で取っていますが、その値は意図したものとは既に異なっています。当然、おかしな動作が起こります。
これを修正するには、プログラムコードの開発者は、各計算動作の中で変数名の紛らわしさを避けるため、次のように書くか、あるいは似たような操作を入れる必要があります。
a = f(...); b = g(...); y[0] = a; y[1] = b;
(訳注:関数 f() と g() の計算結果を一旦一時的な変数に入れ、両方の関数が終わった後に、引数で取った配列 y に値をコピーしている)
あるいは、変数名からくる混乱のリスクが気になる開発者は、入力用と出力用で同じ配列の参照を渡さないようにするかです。私も注意深い方ですが、いずれの解決策を使う場合でも、より注意を払う事が必要です。
では、結論としてどうすればいいでしょうか? 私としては、今の段階では、現状のインターフェースは、受け入れ可能な妥協点かと思います。将来的に可能性のある解決策としては、メモリー確保のコストを大元で取り除くことです。すなわち Arrayクラスの中で何等かのメモリー確保を行う方法です。いつも通り、どんな提案も歓迎します。
最後に、QuantLibライブラリーは、Ⅰ次元の Stochastic Processの為の PathGeneratorクラスも用意している事を述べておきます。いくつか、MultiPathGeneratorとの明確な違いがあります (注: まず StochasticProcess1Dクラスのインターフェースを呼び、MultiPathsではなく Path を生成します。またこのクラスは Brownian Bridgeに (1Dであれば対応が簡単なので)対応しています。)。それ以外は、MultiPathGeneratorクラスと同じ仕組みで動作するので、その詳細の説明は割愛させて頂きます。
<ライセンス表示>
QuantLibのソースコードを使う場合は、ライセンス表示とDisclaimerの表示が義務付けられているので、添付します。 ライセンス