PRML復々習レーン#13に参加して発表してきました

PRML復々習レーン#13に参加して発表してきました.会場を提供してくださった @7shi さん,会場手配にご尽力いただいた @Prunus1350 さんに改めて御礼申し上げます.発表者,参加者のみなさまもありがとうございます.

前回のあらすじをuploadしました.

今日はついにノーテーション地獄である因子グラフ->積和アルゴリズム->max-sumアルゴリズムのセクション.本レーンで読んだときには???の嵐で何も覚えていなかったが,さすがに3度目の正直,ついに (今までに比べて) 深く理解できたという気がした.図や式に対してどんな説明が欠けているのか,今までよく理解できなかった理由がよくわかった気がした.(え,それを本レーンで説明したんじゃないかって?)

ざっくりいうと今日は

  • 無向グラフをさらに一般化した因子グラフの導入
  • 因子グラフにおけるメッセージパッシング手法である積和 (sum-product) アルゴリズムの説明
  • max-sum アルゴリズムの説明

の3つがポイント.

sum-product アルゴリズムは,説明不足な記号があって苦労したものの,なんとか理解することができたと思う.変数ノード->因子ノード,因子ノード->変数ノードのメッセージを分けて書いているけれど,結局再帰的に表現するのでどちらかのメッセージだけで表現することも可能.というか,どちらもμで書いているから目をしばしばさせないと区別できないから記号を変えればよいじゃない,と思ったり.

以前まで有向グラフ <=> 無向グラフ <=> 因子グラフは等価な変換が可能と思い込んでいたけれど,そうではないことも認識できた.PRMLに書いてあるとおり無向グラフではクリークあたり,ひとつのポテンシャル関数しか用意できないが,因子グラフでは複数の因子関数を用意してもよい意味で柔軟である.また,因子グラフでは表現できないノードの親子関係を有向グラフは表現可能である.

あとは図8.44と図8.45,図8.44では有向グラフを因子グラフに「変換」している.ここでは,p(x3|x1,x2)が存在するため,f(x_1, x_2, x_3)の因子が必須 (図8.45のように3つの因子に分解できない) という解釈をした.グラフを変えた時点で失われる情報もあるのから,そこも落としてもよいのかもしれないけれど,図8.45ではそもそも親子関係が不明な無向グラフをスタート地点にしているので,極大クリークにポテンシャル関数を用意しようが,2ノードのクリークにポテンシャル関数を用意しようがどちらでもよい (グラフがそれを規定できないので) ふたつの記述方法を例示している.読み違いかもしれないけれど,きっとそういうことだろう.

最後の最後だけ少し担当.発表資料は以下のとおり.

ジャンクションツリーアルゴリズムについて誤りがあったので訂正.そもそも無向グラフ->三角分割->ジャンクションツリーと変換されたジャンクションツリーそのものは因子グラフとは異なるクラスのグラフである,ということを理解できていなかった.詳細は追っていないけれど,クリークをノード,クリーク間で共有された変数(集合)をリンクとしたグラフの模様.グラフは奥が深い….

有向グラフィカルモデルにおける信念伝播を宿題にさせてもらったのだけれど,PRMLでは信念伝播 (BP) を積和アルゴリズムとは区別しているがWikipediaでは等しいものとして記述されているなど,よくわからない.

まぁなんにせよ一週目はたんなるお経にしか聞こえなかったand見えなかったノーテーション地獄をなんとなくクリアした気分になれたので大変気分がよい.
次回は混合モデルとEM.具体的な例も出てくるのでとても楽しみ.

補足

@K5_semさんに資料を参考にしたといわれて「なんのこっちゃ」と思っていたらPRML本レーンで発表していたことに気づく.全く記憶になかった….というわけで過去のブログ記事と発表資料へのリンクはこちら

菜穂子

(2013-08-14読了)

谷川岳への行き帰りに青空文庫で読んだ.風立ちぬの原案の一部なのかな?

堀辰雄の本は初めて読んだけれど,読みづらい.最初は菜穂子の母親の一人称で始まるのだけれど,母親自身を三人称で呼称したり,人の入れ替わりにどうもついていくのがつらかった.

映画の菜穂子とはかなり違う,それでいて通じるところもある.なお,ストーリーとして収拾がつかず,よくわからないまま終わる.村上春樹小説的な終わり方と捉えることもできるか.

風立ちぬも読んでみよう.

ラムダ式を使ってもう少しかっこよくstd::transformを使ってみる

先日の記事ではstd::transformの使い方を学んだ.しかし,既に用意された関数以外を利用する場合にはその都度関数を定義するのはダサい.どうやらC++ではラムダ式が使えるようになっていたらしいので使ってみたのだけれど,けっこうハマったのでメモ.

先日と同じようなコードをラムダ式を使って書いてみる.

#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
#include <numeric>

#include <cmath>


int
main (int argc, char *argv[])
{

  std::vector<double> vec;
  vec.push_back( 1.0 );
  vec.push_back( 1.5 );
  vec.push_back( 2.0 );
  vec.push_back( 2.5 );

  std::transform(vec.begin(),
                 vec.end(),
                 vec.begin(),
                 std::ptr_fun<double, double>( std::exp ) );

  double sum = std::accumulate(vec.begin(), vec.end(), 0.0);

  //
  std::transform( vec.begin(), vec.end(),
                  vec.begin(),
                  ( [&sum] (double x) -> double { return (x / sum); } ) ); // sum変数を参照キャプチャ

  // 別の方法.こんな書き方もできる
  /*
  std::transform(vec.begin(), vec.end(),
                 vec.begin(),
                 std::bind2nd(
                              std::ptr_fun<double, double, double>( [] (double x, double s) -> double { return (x / s); } ),
                              sum ));
  */
                                         

  for (int i = 0; i < (int)vec.size(); i++) {
    std::cout << vec[ i ] << std::endl;
  }

  return 0;
}

ラムダ式 (<引数>) -> <返り値> {} で書くことができる.ただし,スコープ外部の変数を参照することができないため,最初のの中に参照したい変数を記述する.無印で記述すれば値渡し (すなわち,内部で変更しても外部の変数には影響がない),&を付与すれば参照渡しになる.

コンパイルする際には g++ std=c++0x とstdオプションをつける必要がある.これにけっこうハマった.


なお読み出し元のコンテナとは別のコンテナに値を確保する場合には少しだけ注意が必要.格納先のコンテナにも要素数の大きさのサイズが必要になるため,あらかじめ確保しておく必要がある.

#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
#include <numeric>

#include <cmath>


int
main (int argc, char *argv[])
{

  std::vector<double> vec;
  vec.push_back( 1.0 );
  vec.push_back( 1.5 );
  vec.push_back( 2.0 );
  vec.push_back( 2.5 );


  std::vector<dobule> vec2;
  
  // エラー: vec2のサイズが不足
  std::transform(vec.begin(),
                 vec.end(),
                 vec2.begin(),
                 std::ptr_fun<double, double>( std::exp ) );

  std::vector<double> vec3( vec.size() );

  // OK
  std::transform(vec.begin(),
                 vec.end(),
                 vec3.begin(),
                 std::ptr_fun<double, double>( std::exp ) );

std::vectorのようなSTLコンテナの各要素に指定した関数を適用した結果を得る

std::vectorのようなSTLコンテナの各要素に対して関数を適用した結果を簡単に書きたい.
Perlでいうところのこんなイメージ.

my @list     = (1.0, 1.5, 2.0, 2.5);
my @exp_list = map { exp($_) } @list;

C++ではどう書くのだろう? STLで対応する関数が用意されているだろうなぁと思って手書きしていたので,今回ちゃんと調べてみたので自分用メモ.

結論からいえば,std::transform (algorithmヘッダ) を利用すればよい.ただし,最後の引数が関数オブジェクトである必要があるので,そのままではstd::expのような関数ポインタを利用できない.そこでstd::ptr_fun (functionalヘッダ) という関数アダプタ (と呼ばれるもの) を利用する.C++使いには常識なのだろうけれど,今日初めて知った.

ptr_funは関数ポインタを関数オブジェクトに変換してくれる.class1は引数の型,class2は返り値の型を表す.

言葉で書くよりもコードを載せる方がわかりやすいのでメモ.

#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
#include <cmath>


int
main (int argc, char *argv[])
{

  std::vector<double> vec;
  vec.push_back( 1.0 );
  vec.push_back( 1.5 );
  vec.push_back( 2.0 );
  vec.push_back( 2.5 );

  std::transform(vec.begin(),
                 vec.end(),
                 vec.begin(),
                 std::ptr_fun<double, double>( std::exp ) );

  for (int i = 0; i < (int)vec.size(); i++) {
    std::cout << vec[ i ] << std::endl;
  }

  return 0;
}

なるほど,これで少しは書きやすくなった.

PRML復々習レーン#12に参加して発表してきました

PRML復々習レーン#12に参加して発表してきました.会場を提供してくださったニフティさん,会場係の @who_you_me さん,幹事の @Prunus1350 さん,発表者,参加者のみなさまありがとうございます.改めて感謝申しあげます.

今日発表した前回のあらすじ資料をアップしました.

以下のような質問をいただく.

  • 超事前分布をどこまで取るか?
  • RVMの貢献ってベイズ線形回帰にデータ毎にARD適用したこと? -> YES
  • グラフィカルモデルは誰か提唱したか

元々ベイジアンネットワーク,信念ネットワーク (Belief network) とは別にマルコフ確率場のような手法が出てきて,いつのまにかグラフィカルモデルという言葉が出来て,その分類としてふたつが語られるようになったということ.

久々参加の @shuyo さんの「グラフィカルモデルは矢印のリンクの (2013-07-22修正) あるところに意味があるのではない.矢印のリンクの(2013-07-22修正) ないところに意味がある」というフォローでかなり理解が進む.


ナイーブベイズのところで,矢印反転したらどうなるのか? というところでひと議論.矢印を反転すると,tail-to-tailになってしまうので,クラスが観測されると全ての単語に依存関係が発生してしまう.というわけで矢印の方向はクラスから単語という方向が正しい.

実は1つの依存だけ考えてあげるAverage One-Dependence Estimator (AODE) というものがある.元論文は以下のつぶやきの通り.

"Not So Naive Bayes" というタイトルが大変面白い.そういえばふと思い出したけれど,M1の夏休みだったかを使って研究室でナイーブベイズ輪講というものをやっていたことをふと思い出す.しかし,内容を全く思い出せない.ちょっと資料を漁ってみよう.


条件付き独立については昔本レーンで発表担当した記憶だけあるけれど,すっかり忘れていた.

  • 観測すると親と親 (子) が条件付き独立になる
    • head-to-head
    • head-to-tail
  • 観測すると親と親が独立にならない (観測されなければ親同士は独立)
    • tail-to-tail

これを元にしたd分離について次回の復習資料でフォローするのが宿題.

イジングモデルのところでは,学習に用いるアルゴリズムの違いは組み合わせ最適化をどれだけ正確に解くのか,ということと理解した.ICM (Greedyな方法),Gibbs sampling,Max-prodcut,そして厳密解を求めるグラフカットアルゴリズムで,@naoya_t さんおすすめはグラフカットアルゴリズム

というようなことを感じながら聞いていた.次回は8/24の予定.各自夏休みの課題をやってくるようにと言ってしまったので何をやろうかネタを考えておこう.

勉強会後はひさびさのご飯.エクセルの行列入れ替えの相談をしたり(笑),勉強に関する話をしたり.@shakezo_ さんとMLaPP読みたいけれど,だれか興味を持つ人はいるのだろうか,という話をしたり.なおMLaPPは以下の1000ページ超の武器.

Machine Learning: A Probabilistic Perspective (Adaptive Computation and Machine Learning series)

Machine Learning: A Probabilistic Perspective (Adaptive Computation and Machine Learning series)

右手にPRML (原書),左手にMLaPPを装備すればそこらへんのモンスターは撲殺できるのではないだろうか*1

さて今回からグラフィカルモデル本番に入り,次回はいよいよノーテーション地獄.今度はちゃんと理解できるだろうか.

*1:FF3ネタ.分からない人はFF3 学者で検索してみてください

非復元抽出の高速かつ実装が簡単な方法を考える

※ @tomerun さんに書いてもらったコードとその検証結果を記事の最後に追記しました.(2013-07-21 2:00)

ふとしたきっかけで非復元抽出 (random sampling without replacement) を実装するときに気になったのでどんな実装がよいのか考えてみた.なお非復元抽出はビンゴのように,N個の要素の中からk個の異なる要素をランダムに選択するという意味である.

復元抽出については @unnonouno さんのブログなどに書いてあり,非復元抽出についてもリンクが張ってあったのだけれど,リンク先のブログ記事が読めない状態になっていていたのが残念.

はじめに

std::vectorのようにN個のオブジェクトを格納した配列からk個の異なるオブジェクトをランダムに選択したいというような状況を考える.このような処理を何回も行うため,可能な限り高速に処理したい.

今回は以下の3つの方法を考えた.

  • (1) std::setを用いる方法
  • (2) std::tr1::unordered_setを用いる方法
  • (3) std::vector + std::random_shuffleを用いる方法
  • ※ 本ブログ記事をほぼ書き終わったところでKnuth先生の方法を知ったのであとで追記 orz

(1),(2)は元の配列からランダムに選択してset/unordered_setにinsertして,コンテナの大きさがkになったら停止するというもの.(3)は元の配列の各要素へのポインタを保持したvectorを作成し,それをstd::random_shuffleを用いてシャッフルして先頭からk個取得すればよい.

何も考えずに実装すると,ランダムに要素を選択してsetに挿入する方法を思いつく.しかしsetの内部はmapと同様二分木 (赤黒木) で実装されているので,要素が増えるとコストが大きくなると予想される.というわけでハッシュ実装であるunordered_setの利用を思いつく.そしてNの要素を入れた配列をシャッフルすることができればkの数に依存せずに一定時間で任意の数の非復元抽出ができるじゃないかと思って3番目の方法を思いつく.

きっとここらへんが誰でも思いつくレベルかつ実装が簡単な方法ではないだろうか.というわけでこの3つの方法の速度の差を検証してみる.

実験

実験条件

3つの手法による処理時間を以下のNとkの組み合わせについて10回ずつ計測.今回はkを固定の数字ではなく比率とした.すなわちN=1000, k=0.1の場合には1000個の要素の中から100個の要素を非復元抽出する.

  • N \in {10^3, 10^4,..., 10^8}
  • k \in {0.1, 0.2, ..., 0.9}
  • 測定方法
    • gettimefoday(2)で処理時間を計測
    • ※ random_shuffleの場合はstd::vectorを構築する部分も時間に含める
  • 実験環境
    • gcc 4.4.5 (-O3オプション)
    • Linux kernel 2.6.32-5
    • Core i7 950 (3.07GHz)

実験に利用したコードの一部は以下のとおり.

#define RAND (((double)rand()-1)/((double)RAND_MAX))

double
test_set (int N, int k)
{
  std::vector<int> vec(N);
  for (int i = 0; i < N; i++) {
    vec[ i ] = i;
  }

  double t1 = gettimeofday_sec();
  std::set<int> s;
  while ((int)s.size() < k) {
    int idx = (int)(RAND * N);
    s.insert( vec[ idx ] );
  }
  double t2 = gettimeofday_sec();

  return (t2 - t1);
}

double
test_hash (int N, int k)
{
  std::vector<int> vec(N);
  for (int i = 0; i < N; i++) {
    vec[ i ] = i;
  }

  double t1 = gettimeofday_sec();
  std::tr1::unordered_set<int> s;
  while ((int)s.size() < k) {
    int idx = (int)(RAND * N);
    s.insert( vec[ idx ] );
  }
  double t2 = gettimeofday_sec();

  return (t2 - t1);
}


double
test_shuffle (int N, int k)
{
  std::vector<int> vec(N);
  for (int i = 0; i < N; i++) {
    vec[ i ] = i;
  }

  double t1 = gettimeofday_sec();
  std::vector<int*> shuffle_vec(N);
  for (int i = 0; i < (int)vec.size(); i++) {
    shuffle_vec[ i ] = &(vec[ i ]);
  }

  std::random_shuffle( shuffle_vec.begin(), shuffle_vec.end() );
  double t2 = gettimeofday_sec();

  return (t2 - t1);
}
結果

まずNとkに対する全ての結果を示す.表が見づらくなるので平均値のみを記載.(分散は結論に影響を与えない程度の大きさだった)

N k (ratio) set unordered_set random_shuffle
1000 0.1 0.000026 0.000023 0.000042
1000 0.2 0.000055 0.000045 0.000042
1000 0.3 0.000086 0.000062 0.000043
1000 0.4 0.000123 0.000081 0.000043
1000 0.5 0.000143 0.000054 0.000021
1000 0.6 0.000104 0.000067 0.000021
1000 0.7 0.000134 0.000078 0.000020
1000 0.8 0.000175 0.000092 0.000021
1000 0.9 0.000245 0.000122 0.000021
10000 0.1 0.000142 0.000097 0.000215
10000 0.2 0.000310 0.000206 0.000205
10000 0.3 0.000506 0.000290 0.000217
10000 0.4 0.000744 0.000461 0.000205
10000 0.5 0.001025 0.000543 0.000207
10000 0.6 0.001380 0.000665 0.000212
10000 0.7 0.001823 0.000809 0.000205
10000 0.8 0.002451 0.001154 0.000211
10000 0.9 0.003540 0.001388 0.000212
100000 0.1 0.0020 0.0012 0.0022
100000 0.2 0.0046 0.0025 0.0022
100000 0.3 0.0083 0.0039 0.0022
100000 0.4 0.0123 0.0058 0.0022
100000 0.5 0.0171 0.0073 0.0022
100000 0.6 0.0232 0.0090 0.0022
100000 0.7 0.0311 0.0121 0.0022
100000 0.8 0.0421 0.0143 0.0022
100000 0.9 0.0606 0.0177 0.0022
1000000 0.1 0.0324 0.0153 0.0265
1000000 0.2 0.0819 0.0364 0.0262
1000000 0.3 0.1547 0.0774 0.0264
1000000 0.4 0.2447 0.1005 0.0264
1000000 0.5 0.3583 0.1280 0.0263
1000000 0.6 0.5024 0.1990 0.0265
1000000 0.7 0.6924 0.2338 0.0267
1000000 0.8 0.9639 0.2818 0.0268
1000000 0.9 1.4319 0.3625 0.0264
10000000 0.1 0.7067 0.3181 0.6920
10000000 0.2 1.7642 0.7155 0.6909
10000000 0.3 3.0718 1.2310 0.6921
10000000 0.4 4.6572 1.5806 0.6909
10000000 0.5 6.5762 2.3766 0.6904
10000000 0.6 8.9766 2.7640 0.6929
10000000 0.7 12.1109 3.2506 0.6924
10000000 0.8 16.5619 3.9203 0.6911
10000000 0.9 24.2751 5.9600 0.6909
100000000 0.1 11.9186 5.0229 9.4275
100000000 0.2 28.4891 10.7473 9.4202
100000000 0.3 48.9235 14.4396 9.4179
100000000 0.4 73.7326 23.2700 9.4068
100000000 0.5 103.9370 27.3172 9.4198
100000000 0.6 141.7940 32.3755 9.4193
100000000 0.7 191.4050 38.9861 9.4176
100000000 0.8 262.2190 57.5724 9.4168
100000000 0.9 384.7380 71.8925 9.4186

手法毎にNとkの組み合わせについての3次元棒グラフは以下のとおり.上段が10^3~6まで.下段が10^3~5までの結果を全て同じ尺度で示している.

実験より,以下の結果を確認した.

  • k=0.1の場合に実行速度は unordered_set > random_shuffle > set
  • kが0.2以上の場合に実行速度は random_shuffle > unordered_set > set
  • なぜかN=1000,k=0.1-0.4のrandom_shuffleが遅い.(何回かやり直しても再現)

なおrandom_shuffle実装においてポインタ要素の配列を作成する処理を時間計測から外した場合においても上記の結果は変わらなかった.

まとめ

上記の結果より,以下の知見を得た.

  • あらゆる状況においてsetは遅い.
  • 非復元抽出を行う要素数が少ない場合には(2)の方法を用いる
  • 復元抽出を行う要素数が抽出元サイズの2割以上の場合には(3)の方法を用いる
  • ポインタ要素の配列作成のコストが小さいことから配列操作は高速ということがわかる.

やはりtreeはポインタをたどるのでキャッシュミスが発生しやすい->遅い,という印象がさらに強まる結果であった.N=1000,k=0.1-0.4のrandom_shuffleが遅いのはrandom_shuffleの実装を調べればわかるのだろうか? 配列の大きさが小さい場合には,ランダム性を保証するためにたくさんシャッフルするとか?

さよならset,君のことは忘れないよ.(あれ,どっかで聞いたことあるセリフ?)

発展手法について

どうやらKnuth先生の本に非復元抽出の高速なアルゴリズムが載っているらしい.
(参考: http://stackoverflow.com/questions/311703/algorithm-for-sampling-without-replacement)

今回の実験に合わせた表記にするとこんな感じ.

double
test_knuth (int N, int k)
{
  std::vector<int> vec(N);
  for (int i = 0; i < N; i++) {
    vec[ i ] = i;
  }

  double t1 = gettimeofday_sec();

  std::vector<int> sample_vec(k);

  int t = 0;
  int m = 0;
  while (m < k) {
    double u = RAND;
    if ((N - t) * u >= (k - m)) {
      t++;
    } else {
      sample_vec[ m ] = vec[ t ];
      t++;
      m++;
    }
  }
  double t2 = gettimeofday_sec();

  return (t2 - t1);
}

ざっと試してみたところ,どうやら一番高速そう.しかも実装も簡単.今回の実験で表とグラフを作ったあとにこの方法を知ったので相当げんなりしている.グラフを作り直す元気はなかったので,更新した表を最後に追記.

また今回は簡単な実装ということで効率的な実装について深追いしなかったけれど,論文になっている手法では以下のようなものがあるらしい.ざっと読んだけれどまったく理解できなかったので見なかったことにした.

Appendix

Knuthの方法の結果を以下に追記.

結果は以下の解釈をしている.

  • Nとkの数が共に小さい場合にはunordered_set
  • それ以外の場合にはKnuth

結論: Knuth先生すごい!

N k (ratio) set unordered_set random_shuffle knuth
1000 0.1 0.000026 0.000023 0.000042 0.000025
1000 0.2 0.000055 0.000045 0.000042 0.000030
1000 0.3 0.000086 0.000062 0.000043 0.000034
1000 0.4 0.000123 0.000081 0.000043 0.000038
1000 0.5 0.000143 0.000054 0.000021 0.000040
1000 0.6 0.000104 0.000067 0.000021 0.000118
1000 0.7 0.000134 0.000078 0.000020 0.000036
1000 0.8 0.000175 0.000092 0.000021 0.000032
1000 0.9 0.000245 0.000122 0.000021 0.000028
10000 0.1 0.000142 0.000097 0.000215 0.000253
10000 0.2 0.000310 0.000206 0.000205 0.000300
10000 0.3 0.000506 0.000290 0.000217 0.000184
10000 0.4 0.000744 0.000461 0.000205 0.000186
10000 0.5 0.001025 0.000543 0.000207 0.000195
10000 0.6 0.001380 0.000665 0.000212 0.000189
10000 0.7 0.001823 0.000809 0.000205 0.000170
10000 0.8 0.002451 0.001154 0.000211 0.000150
10000 0.9 0.003540 0.001388 0.000212 0.000134
100000 0.1 0.0020 0.0012 0.0022 0.0012
100000 0.2 0.0046 0.0025 0.0022 0.0014
100000 0.3 0.0083 0.0039 0.0022 0.0017
100000 0.4 0.0123 0.0058 0.0022 0.0018
100000 0.5 0.0171 0.0073 0.0022 0.0019
100000 0.6 0.0232 0.0090 0.0022 0.0019
100000 0.7 0.0311 0.0121 0.0022 0.0017
100000 0.8 0.0421 0.0143 0.0022 0.0015
100000 0.9 0.0606 0.0177 0.0022 0.0013
1000000 0.1 0.0324 0.0153 0.0265 0.0121
1000000 0.2 0.0819 0.0364 0.0262 0.0144
1000000 0.3 0.1547 0.0774 0.0264 0.0166
1000000 0.4 0.2447 0.1005 0.0264 0.0184
1000000 0.5 0.3583 0.1280 0.0263 0.0192
1000000 0.6 0.5024 0.1990 0.0265 0.0187
1000000 0.7 0.6924 0.2338 0.0267 0.0170
1000000 0.8 0.9639 0.2818 0.0268 0.0150
1000000 0.9 1.4319 0.3625 0.0264 0.0129
10000000 0.1 0.7067 0.3181 0.6920 0.1222
10000000 0.2 1.7642 0.7155 0.6909 0.1451
10000000 0.3 3.0718 1.2310 0.6921 0.1675
10000000 0.4 4.6572 1.5806 0.6909 0.1860
10000000 0.5 6.5762 2.3766 0.6904 0.1943
10000000 0.6 8.9766 2.7640 0.6929 0.1887
10000000 0.7 12.1109 3.2506 0.6924 0.1729
10000000 0.8 16.5619 3.9203 0.6911 0.1529
10000000 0.9 24.2751 5.9600 0.6909 0.1368
100000000 0.1 11.9186 5.0229 9.4275 1.2284
100000000 0.2 28.4891 10.7473 9.4202 1.4638
100000000 0.3 48.9235 14.4396 9.4179 1.6919
100000000 0.4 73.7326 23.2700 9.4068 1.8807
100000000 0.5 103.9370 27.3172 9.4198 1.9683
100000000 0.6 141.7940 32.3755 9.4193 1.9175
100000000 0.7 191.4050 38.9861 9.4176 1.7639
100000000 0.8 262.2190 57.5724 9.4168 1.5684
100000000 0.9 384.7380 71.8925 9.4186 1.3664

2013-07-21 2:00 追記 (Thanks to @tomerun さん)

ブログ記事upのつぶやきをしたら @tomerun さんから以下の返信を頂く.

ありがとうございます! @tomerun さんに書いてもらったコードの該当部分だけを転載します.

// Code by @tomerun さん
// http://ideone.com/raF3O4 から引用

double
test_shuffle_partial (int N, int k)
{
  std::vector<int> vec(N);
  for (int i = 0; i < N; i++) {
    vec[ i ] = i;
  }
 
  double t1 = gettimeofday_sec();
  std::vector<int> shuffle_vec;
 
  for (int i = 0; i < k; ++i) {
        int pos = rand() % (N-i);
        shuffle_vec.push_back(vec[i + pos]);
        swap(vec[i], vec[i + pos]);
  }
 
  double t2 = gettimeofday_sec();
 
  return (t2 - t1);
}

これを見ると,選択された要素以外からランダムに選ぶ.選択されたものを配列の先頭に移して,それ以外からランダムに要素を選ぶ,ということを繰り返してk個のアイテムを取得する.現実世界におけるビンゴのような非復元抽出手法をコードに落とし込んだ感じ.なるほど,こう実装すればよいのか! 目からうろこです.

なお,超細かいところで恐縮なのだけれどpush_backするとkが大きい場合に何度もvectorのreserve (realloc) が走るので,std::vector shuffle_vec(k)として,push_backの代わりにshuffle_vec[ i ] = vec[ i + pos ];とするとkが大きいときに少し速くなる.N=10^8,k=0.9で5.2277->4.972程度.まぁやればできる系の高速化なのでtrivialですが.

さぁさぁ同じ環境で実行して以下の結果を得た.random_shuffle_partial以外は前回の数字.

N k (ratio) set unordered_set random_shuffle knuth random_shuffle_partial
1000 0.1 0.000026 0.000023 0.000042 0.000025 0.000005
1000 0.2 0.000055 0.000045 0.000042 0.000030 0.000007
1000 0.3 0.000086 0.000062 0.000043 0.000034 0.000010
1000 0.4 0.000123 0.000081 0.000043 0.000038 0.000013
1000 0.5 0.000143 0.000054 0.000021 0.000040 0.000015
1000 0.6 0.000104 0.000067 0.000021 0.000118 0.000019
1000 0.7 0.000134 0.000078 0.000020 0.000036 0.000021
1000 0.8 0.000175 0.000092 0.000021 0.000032 0.000024
1000 0.9 0.000245 0.000122 0.000021 0.000028 0.000026
10000 0.1 0.000142 0.000097 0.000215 0.000253 0.000113
10000 0.2 0.000310 0.000206 0.000205 0.000300 0.000059
10000 0.3 0.000506 0.000290 0.000217 0.000184 0.000087
10000 0.4 0.000744 0.000461 0.000205 0.000186 0.000112
10000 0.5 0.001025 0.000543 0.000207 0.000195 0.000091
10000 0.6 0.001380 0.000665 0.000212 0.000189 0.000081
10000 0.7 0.001823 0.000809 0.000205 0.000170 0.000096
10000 0.8 0.002451 0.001154 0.000211 0.000150 0.000113
10000 0.9 0.003540 0.001388 0.000212 0.000134 0.000128
100000 0.1 0.0020 0.0012 0.0022 0.0012 0.0002
100000 0.2 0.0046 0.0025 0.0022 0.0014 0.0003
100000 0.3 0.0083 0.0039 0.0022 0.0017 0.0005
100000 0.4 0.0123 0.0058 0.0022 0.0018 0.0008
100000 0.5 0.0171 0.0073 0.0022 0.0019 0.0009
100000 0.6 0.0232 0.0090 0.0022 0.0019 0.0011
100000 0.7 0.0311 0.0121 0.0022 0.0017 0.0013
100000 0.8 0.0421 0.0143 0.0022 0.0015 0.0014
100000 0.9 0.0606 0.0177 0.0022 0.0013 0.0015
1000000 0.1 0.0324 0.0153 0.0265 0.0121 0.0020
1000000 0.2 0.0819 0.0364 0.0262 0.0144 0.0040
1000000 0.3 0.1547 0.0774 0.0264 0.0166 0.0068
1000000 0.4 0.2447 0.1005 0.0264 0.0184 0.0089
1000000 0.5 0.3583 0.1280 0.0263 0.0192 0.0109
1000000 0.6 0.5024 0.1990 0.0265 0.0187 0.0136
1000000 0.7 0.6924 0.2338 0.0267 0.0170 0.0155
1000000 0.8 0.9639 0.2818 0.0268 0.0150 0.0174
1000000 0.9 1.4319 0.3625 0.0264 0.0129 0.0191
10000000 0.1 0.7067 0.3181 0.6920 0.1222 0.0514
10000000 0.2 1.7642 0.7155 0.6909 0.1451 0.1022
10000000 0.3 3.0718 1.2310 0.6921 0.1675 0.1537
10000000 0.4 4.6572 1.5806 0.6909 0.1860 0.2010
10000000 0.5 6.5762 2.3766 0.6904 0.1943 0.2535
10000000 0.6 8.9766 2.7640 0.6929 0.1887 0.2976
10000000 0.7 12.1109 3.2506 0.6924 0.1729 0.3389
10000000 0.8 16.5619 3.9203 0.6911 0.1529 0.3756
10000000 0.9 24.2751 5.9600 0.6909 0.1368 0.4156
100000000 0.1 11.9186 5.0229 9.4275 1.2284 0.6073
100000000 0.2 28.4891 10.7473 9.4202 1.4638 1.2054
100000000 0.3 48.9235 14.4396 9.4179 1.6919 1.7780
100000000 0.4 73.7326 23.2700 9.4068 1.8807 2.3940
100000000 0.5 103.9370 27.3172 9.4198 1.9683 2.9615
100000000 0.6 141.7940 32.3755 9.4193 1.9175 3.5221
100000000 0.7 191.4050 38.9861 9.4176 1.7639 4.1683
100000000 0.8 262.2190 57.5724 9.4168 1.5684 4.7086
100000000 0.9 384.7380 71.8925 9.4186 1.3664 5.2277

さて実験から以下の結果を得た.

  • N<=10^6までは全ての手法でrandom_shuffle_partialが最速
  • 全てのN,kにおいてrandom_shuffle_partial>random_shuffle
  • Knuth vs. random_shuffle_parial 最速対決
    • N=10^7,k<0.4においてrandom_shuffle_partial>Knuth>その他
    • N=10^7,k>=0.4においてKnuth>random_shuffle_partial>その他
    • N=10^8,k<0.3においてrandom_shuffle_partial>Knuth>その他
    • N=10^8,k>=0.3においてKnuth>random_shuffle_partial>その他

Nとkが大きくなるとknuth先生に負けてしまう.おそらくkの絶対数が増えるとswapコストが効いてくるのだろうか.しかし,実験のためk=0.1-0.9を検証しているけれど,実際にはkはそんなに大きくない場合の方が多いので,@tomerun さん手法の方が高速な場面の方が多いと思われる.

改めて@tomerun さんに感謝.さすができる人は対応もコードも3倍の速さですね!

「超」入門 失敗の本質 日本軍と現代日本に共通する23の組織的ジレンマ

「超」入門 失敗の本質 日本軍と現代日本に共通する23の組織的ジレンマ

「超」入門 失敗の本質 日本軍と現代日本に共通する23の組織的ジレンマ

(2013-07-19読了)

会社の先輩と組織論などの話になったときに「『失敗の本質』読んだ?読んでないならオススメ.最近その解説本出たみたいだけど読んでないや」と言われて,まず解説本から読むというゆとりっぷりを発揮して読む.

「失敗の本質」は第二次世界大戦において日本が失敗した6つの作戦を題材に,なぜ失敗したのかということを考察した本であり,組織や経営を考えていく上でバイブル的な本らしい.

そもそも「失敗の本質」を読んでいないので,これがよい解説本なのかわからないけれど,割とよくあるハウツー本のような印象を受けた.ざっと以下のようなことが印象に残っている.

  • 目的に合わせた新たな指標を設けることの重要さ
  • 現在の指標の根底を失わせるためのイノベーションが必要
  • ダブル・ループ学習

最後のダブル・ループ学習は"「想定した目標や問題自体が間違っているのではないか」という疑問・検討を含めた学習法 (p.121)"だそうだ.たぶん日本の会社ではPDCA()という言葉が連呼されていると思うけれど,たいていの場合には本書でいうところのシングルループ学習,すなわち「想定した目標や価値観を固定した」営みになっているのではないだろうか.著者によれば日本人は一度決めた指標に対して最適化をするのは得意だが,その指標を適切なタイミングで変えるのがなかなか苦手らしい.自分も割とそのタイプだと思う.

さて,引用の引用の引用になってしまうが,以下の箇所は心に刺さった.

左記は『失敗の本質』3章で引用されている、日本軍の組織がいかに平和時に安定していたか、という指摘です。

日本軍人(陸海軍)は、
・思索せず
・読書せず
・上級者となるに従って反駁する人もなく
・批判をうける機会もなく
・式場の御神体となり
・権威の偶像となって
・温室の裡に保護された
(「太平洋海戦史』高木惣吉岩波新書より)

まるで、現代日本のどこかの組織内情を暴露しているような印象を抱かないでしょうか。
(p.200)