前回の話題のヒント
書いている時間がないのでヒントを
- 巡回セールスマン問題は対称群を引数に持つコスト関数の最小化問題とみなせる。
- 対称群は隣同士の互換から生成できる。
という事実をアニーリングに適用すると得られる。
ただし、スケーリングに関しての収束性のよさは不明。 すこし調べた感じでは、誰もこの方法をやっていなかった*1。
改善方法としては、互換全体を元とするということも考えられる。 収束が若干早くなる可能性がある。
もちろん巡回セールスマン問題は、この上なくよく考察されている問題の一つなので、 ほかにもいろいろな解法や準解法がある。 とくに、近似アルゴリズムは考えておくべき。
*1:やっぱり、スケーリングに対する収束速度が悪いのかな?
ボルツマンマシンとはちょっと違うけど
bocchi-talks-information.hatenablog.com
をシミュレーティッドアニーリング的に(準)最適解を求める方法を少し考えたので、メモ的にソースコードだけでも。
解説はそのうちやります。
#define flp(i, n) for(int i = 0; i < n; ++i) #include <iostream> #include <bitset> #include <cmath> #include <iomanip> #include <random> using namespace std; int main() { random_device rd; mt19937 mt(rd()); uniform_real_distribution<double> score(0.0,1.0); int dist[7][7] = { { 0,124,127,125, 26, 54, 42}, {124, 0, 77,165,116,143,150}, {127, 77, 0,104,111,182,165}, {125,165,104, 0,110,164,137}, { 26,116,111,110, 0, 66, 65}, { 54,143,182,164, 66, 0, 77}, { 42,150,165,137, 65, 77, 0} }; int minn = 10000; int prm[8], mprm[8]; flp(k, 100){ prm[0] = prm[7] = 0; flp(i, 6) prm[i + 1] = i + 1; double T = 1000.0; flp(i, 200000){ int j = i % 5 + 1; double dene = dist[ prm[j - 1] ][prm[j + 1] ] - dist[ prm[j - 1] ][prm[j] ] + dist[ prm[j] ][prm[j + 2] ] - dist[ prm[j + 1] ][prm[j + 2] ]; double p = 1.0 / ( 1.0 + exp( dene/T)); double sc = score(mt); if(p > sc){int buf = prm[j]; prm[j] = prm[j + 1]; prm[j + 1] = buf;} if(i % 1000) T *= 0.9; } int sum = 0; flp(i, 7) sum += dist[ prm[i] ][prm[i + 1] ]; if( minn > sum) { flp(i, 8) mprm[i] = prm[i]; minn = sum;} } flp(i, 8) cout << mprm[i] << " "; cout << ": " << minn <<endl; return 0; }
一応これでほとんど最適解にたどり着くはず。
巡回セールスマン問題とボルツマンマシン
今回は、少し趣向を変えてボルツマンマシンが使われている例をみたいと思う。
参考にしたサイトは次のサイト。基本的にすべてここに書いてあることを焼きなおししようと思う。
ただし、今回は25地点のリンクや重みを作るのが面倒だったため、関東地方の県庁所在地を車で移動した際にかかる距離で話を進めたいと思う。
問題設定
まずは関東地方の地図を見てもらいたい。世界地図
世界地図・日本地図の白地図を無料ダウンロード!【世界地図|SEKAICHIZU】
という白地図を提供していただけるサイトを参考にすると、関東地方は
となっている。googleによると、これらの都市間の車での移動距離は単位をkmとして、
水戸 | 宇都宮 | 前橋 | さいたま | 千葉 | 東京 | 横浜 | |
---|---|---|---|---|---|---|---|
水戸 | 0 | 77 | 165 | 116 | 143 | 124 | 150 |
宇都宮 | 77 | 0 | 104 | 111 | 182 | 127 | 165 |
前橋 | 165 | 104 | 0 | 110 | 164 | 125 | 137 |
さいたま | 116 | 111 | 110 | 0 | 66 | 26 | 65 |
千葉 | 143 | 182 | 164 | 66 | 0 | 54 | 77 |
東京 | 124 | 127 | 125 | 26 | 54 | 0 | 42 |
横浜 | 150 | 165 | 137 | 65 | 77 | 42 | 0 |
らしい*1。
これらの都市を東京から出発して一回ずつまわり、最後に東京に戻ってくるその経路の中で最短なものはどれかと言う問題を考えよう。例のサイトにならって、
都市を表す変数を、今何番目の都市かというのを表す変数をとして、番目ににいるという変数を
で表すとしよう。この変数が1であるならば番目ににいるということになる。
さて、都市名をそのまま扱うのは少し煩雑なので、
水戸 | 宇都宮 | 前橋 | さいたま | 千葉 | 東京 | 横浜 | |
---|---|---|---|---|---|---|---|
番号 | 1 | 2 | 3 | 4 | 5 | 0 | 6 |
と番号付けしておく。ここで、最初と最後が東京(番号0)なので、
がいえる。
また、同時に二つの都市に行かないだとか、同じ都市に2度行かないと言う条件は、
となり、ある1つの経路の距離は、XとYとの距離をとすれば、
と表せる。結局エネルギー関数は十分に大きな実数を用いて、
と表される。また、に注意して、エネルギーの定数項は無視できることを考えると、
と表せる。なお、Gibbs samplingに用いる平均場エネルギーは
となっている。
シミュレーティッド・アニーリング
今回は計算論的焼きなまし法とも言われる方法を用いて、最適解を確率的に計算する。
焼きなまし法(アニーリング)は物性分野(半導体物性とか)でも使われる言葉で、高温で焼いた金属や半導体などをゆっくりと冷やしていくことできれいな結晶を得ると言うものだ*2。
ともかく、シミュレーティッド・アニーリングには温度と言う要素が必要不可欠になる。具体的には、温度を導入して、ギブスサンプリング的なことを行うのがシミュレーティッド・アニーリングになる。
つまり、
を用いて徐々に温度を下げながら*3ユニットを更新していき、最終的な値を得ると言う方法だ。
計算してみる
今回もIdeoneに登場してもらって計算してみようと思う。
今回は都市数が少ないので事前に答えを求めておくことができる。 次のC++14のコード
#include <iostream> #include <algorithm> #include <vector> #include <random> using namespace std; int main() { const int N = 6; int dist[7][7] = { { 0,124,127,125, 26, 54, 42}, {124, 0, 77,165,116,143,150}, {127, 77, 0,104,111,182,165}, {125,165,104, 0,110,164,137}, { 26,116,111,110, 0, 66, 65}, { 54,143,182,164, 66, 0, 77}, { 42,150,165,137, 65, 77, 0} }; int min = 10000; vector<int> v(N), minv(N); iota(v.begin(), v.end(), 1); do { int lsum = 0; int i = 0; for(i = 0; i < 6; ++i ){ if(i == 0){lsum = dist[0][v[i]];} else{lsum += dist[v[i -1]][v[i]];} if(lsum > min)break; } if(lsum + dist[v[i-1]][0] < min){ min = lsum + dist[v[i-1]][0]; minv = v; } } while( next_permutation(v.begin(), v.end()) ); cout << min << endl; for(auto x : minv) cout << x << " "; cout << "\n"; return 0; }
に従うと0→4→3→2→1→5→6→0で579kmつまり
東京→さいたま→前橋→宇都宮→水戸→千葉→横浜→東京で579kmが最短だと言うのが分かる。
シミュレーティッド・アニーリングによる計算
ここで、としておく。
次のコードで挑戦してみるが、いまいち駄目。ミニマムに落ちるようにロールバックするという対策もあるけど、どうなんだろう。もう少しトライが必要みたい。
#include <iostream> #include <bitset> #include <cmath> #include <iomanip> #include <random> using namespace std; int main() { random_device rd; mt19937 mt(rd()); uniform_real_distribution<double> score(0.0,1.0); double T = 100.0; int dist[7][7] = { { 0,124,127,125, 26, 54, 42}, {124, 0, 77,165,116,143,150}, {127, 77, 0,104,111,182,165}, {125,165,104, 0,110,164,137}, { 26,116,111,110, 0, 66, 65}, { 54,143,182,164, 66, 0, 77}, { 42,150,165,137, 65, 77, 0} }; double p_ene[7][8]; bool g_sample[7][8] = {0}; g_sample[0][0] = true; g_sample[0][7] = true; double ene = 0; double pre_ene = 0; for(int j = 1; j < 7; ++j){ for(int k = 1; k < 7; ++k){ g_sample[j][k] = true; } } int g_sample_num = 4000000; for(int i = 0; i < g_sample_num; ++i){ for(int j = 1; j < 7; ++j){ for(int k = 1; k < 7; ++k){ int psum = 0; for(int l = 1; l < 7; ++l){ if(l != j) psum += g_sample[l][k]; if(l != k) psum += g_sample[j][l]; } p_ene[j][k] = 90*( 1 - psum); psum = 0; for(int l = 1; l < 7; ++l){ if(l != j) psum -= dist[l][j] * (g_sample[l][k-1] + g_sample[l][k+1]); } p_ene[j][k] += psum; } } int j_site = (i % 36) % 6 + 1, k_site = (i % 36) / 6 + 1; double p = 1.0 / ( 1.0 + exp(p_ene[j_site][k_site])/T); double sc = score(mt); g_sample[j_site][k_site] = (sc > p); if(i % 15000 == 0){ pre_ene = ene; ene = 0; for(int j = 1; j < 7; ++j){ for(int k = 1; k < 7; ++k){ for(int l = 1; l < 7; ++l){ if(l != j) ene += dist[l][j] *g_sample[j][k] * (g_sample[l][k-1] + g_sample[l][k+1]); } } } int prev = 0; int sumd = 0; for(int k = 1; k < 7; ++k){ for(int j = 1; j < 7; ++j){ if(g_sample[j][k]) { //cout << j << " "; sumd += dist[prev][j]; prev = j; } } } sumd += dist[prev][0]; //cout << sumd << endl; if(ene == pre_ene) T *= 0.95; } } int prev = 0; int sumd = 0; for(int k = 1; k < 7; ++k){ for(int j = 1; j < 7; ++j){ if(g_sample[j][k]) { cout << j << " "; sumd += dist[prev][j]; prev = j; } } } cout << endl; sumd += dist[prev][0]; cout << sumd << endl; cout << T << endl; return 0; }
1時間トライしても駄目。現時点での戒めもかねて公開。
Gibbs sampling について
ボルツマンマシンの状態数
前回はなしたボルツマンマシンだけれど、実際に学習を実行しようとなると、あるパラメータにおける期待値を計算する必要がある。 期待値は例えば、
というようなもので、ボルツマンマシンは上の確率分布だったから、が大きくなると足す必要のある数は爆発的に大きくなる。 要するに、実際の計算機ではやっていられなくなる。そこでちょっとした工夫が必要になる。
全結合ボルツマンマシンの例
今回は次のボルツマンマシンを使う。
0 | -2 | -1 | 3 |
-2 | 0 | -1 | 2 |
-1 | -1 | 0 | 2 |
3 | 2 | 2 | 0 |
このボルツマンマシンの分布は次のようになる。
状態 | 確率(%) |
---|---|
0000 | 0.06 |
0001 | 0.16 |
0010 | 0.00 |
0011 | 0.03 |
0100 | 0.02 |
0101 | 0.02 |
0110 | 0.00 |
0111 | 0.00 |
1000 | 0.42 |
1001 | 23.02 |
1010 | 0.09 |
1011 | 37.95 |
1100 | 1.15 |
1101 | 23.02 |
1110 | 0.09 |
1111 | 13.96 |
ちなみにこの分布を求めて、そこからサンプリングして経験分布を得るC++14のコードは次のようになる*1。
#include <iostream> #include <bitset> #include <cmath> #include <iomanip> #include <random> using namespace std; int main() { random_device rd; mt19937 mt(rd()); uniform_real_distribution<double> score(0.0,1.0); double energy[16] = {0}; double e_ene[16] = {0}; double prob[16]; double i_prob[16] = {0}; double t_e_ene = 0; double b[4] = {1,-3.5,-1,2}; double w[4][4] = { { 0, 2,-1, 3}, { 2, 0,-1, 2}, {-1,-1, 0, 2}, { 3, 2, 2, 0} }; for(int i = 0; i < 16; ++i){ bitset<4> sample(i); for(int j = 0; j < 4; ++j){ energy[i] -= sample[j] * b[j]; for(int k = j + 1; k < 4; ++k){ energy[i] -= sample[j] * sample[k] *w[j][k]; } } e_ene[i] = exp( -energy[i] ); t_e_ene += e_ene[i]; } for(int i = 0; i < 16; ++i){ prob[i] = e_ene[i] / t_e_ene; if( i ) {i_prob[i] = i_prob[i-1] + prob[i];} else{i_prob[i] = prob[i];} } int n_count[16] = {0}; int sample_num = 1000000; for(int j = 0; j < sample_num; ++j){ double sc = score(mt); for(int i = 0; i < 16; ++i){ if(i_prob[i] > sc){ n_count[i]++ ; break; } } } for(int i = 0; i < 16; ++i){ cout << static_cast< bitset<4> >(i) << fixed << setprecision(2)<<" " << (double) 100 * n_count[i] / sample_num <<" " << prob[i] * 100 << endl; } return 0; }
Gibbs sampling
このサンプリング法では、すべての状態についての確率を求めていた*2。 しかし、が増えれば増えるほど、このやり方は無理になっていく。
ここで、ボルツマンマシンの構造からギブスサンプリングを用いることを考える。
ギブスサンプリングとは、ある状態のあるユニットに注目し、他のユニットの値は定まっているものとして、その注目しているユニットの値を更新していく方法で、 十分長い間この更新を繰り返すと所望の分布から生成されたサンプルが得られると理論的には保証されている方法だ。
あるユニットに注目したときに、他のユニットの値が定まっているときのユニットの分布は、
で定まる。
この分布を用いてサンプルを行い、もとの分布と比較するコードを次に紹介する。
#include <iostream> #include <bitset> #include <cmath> #include <iomanip> #include <random> using namespace std; int main() { random_device rd; mt19937 mt(rd()); uniform_real_distribution<double> score(0.0,1.0); double energy[16] = {0}; double e_ene[16] = {0}; double prob[16]; double i_prob[16] = {0}; double t_e_ene = 0; double b[4] = {1,-3.5,-1,2}; double w[4][4] = { { 0, 2,-1, 3}, { 2, 0,-1, 2}, {-1,-1, 0, 2}, { 3, 2, 2, 0} }; double p_ene[4]; bitset<4> g_sample (0); int g_n_count[16] = {0}; int g_sample_num = 1000000; int g_s_t = 0; for(int i = 0; i < g_sample_num; ++i){ for(int j = 0; j < 4; ++j){ p_ene[j] = b[j]; for(int k = 0; k < 4; ++k){ if(k != j) p_ene[j] += w[k][j] * g_sample[k]; } } int i_site = i % 4; double p = 1.0 / ( 1.0 + exp(p_ene[i_site])); double sc = score(mt); g_sample[i_site] = (sc > p); if((i > g_sample_num / 10) && (i % 51 == 0) ){ int nnn = g_sample.to_ulong(); g_n_count[nnn]++; g_s_t++; } } for(int i = 0; i < 16; ++i){ bitset<4> sample(i); for(int j = 0; j < 4; ++j){ energy[i] -= sample[j] * b[j]; for(int k = j + 1; k < 4; ++k){ energy[i] -= sample[j] * sample[k] *w[j][k]; } } e_ene[i] = exp( -energy[i] ); t_e_ene += e_ene[i]; } for(int i = 0; i < 16; ++i){ prob[i] = e_ene[i] / t_e_ene; if( i ) {i_prob[i] = i_prob[i-1] + prob[i];} else{i_prob[i] = prob[i];} } for(int i = 0; i < 16; ++i){ cout << static_cast< bitset<4> >(i) << fixed << setprecision(2)<<" " << (double) 100 * g_n_count[i] / g_s_t <<" " << prob[i] * 100 << endl; } return 0; }
このコードでは状態を0000から始めて、更新回数の10分の1をburn-in期間において51回の更新毎にサンプリングしてある。 結果の一例を次に書く*3。
状態 | Gibbs samplingによる経験分布の確率(%) | 計算による真の確率(%) |
---|---|---|
0000 | 0.06 | 0.06 |
0001 | 0.17 | 0.16 |
0010 | 0.00 | 0.00 |
0011 | 0.02 | 0.03 |
0100 | 0.03 | 0.02 |
0101 | 0.02 | 0.02 |
0110 | 0.00 | 0.00 |
0111 | 0.00 | 0.00 |
1000 | 0.37 | 0.42 |
1001 | 23.06 | 23.02 |
1010 | 0.14 | 0.09 |
1011 | 37.85 | 37.95 |
1100 | 1.14 | 1.15 |
1101 | 23.43 | 23.02 |
1110 | 0.07 | 0.09 |
1111 | 13.65 | 13.96 |
サンプル数が少ないかもしれないが、まずまず良い結果を得られていると思われる。
このギブスサンプリングを用いてあるパラメータにおける期待値を近似的に求めることができる。 これが、ボルツマンマシンの学習に重要となってくる。
どう重要になってくるかは、後日書こうと思う。
ボルツマンマシンとは
本の紹介
基本的には、
- 作者: 岡谷貴之
- 出版社/メーカー: 講談社
- 発売日: 2015/04/08
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (6件) を見る
の最終章をいろいろと端折りながら紹介する。
ボルツマンマシンの状態
すごく大雑把に言えば、ボルツマンマシンとは指数型分布族の一種のこと。 下の絵の様な構造を持っている。
ここで、丸はユニットとよばれ、それぞれ0か1の値をとり、ユニットの全体が確率変数となる。 つまりユニットが個あった場合、そのボルツマンマシンは上の確率分布になっている。 上の絵の場合はユニットが10個あるので、とりうる値としては0000000000から1111111111までの1024個あることになる。
ここで、このとる値のことを状態と呼ぶことにして、で表すことにする。 つまり絵の場合は、だ。
また、番目のユニットがとる値をで表すことにする。
エネルギーと確率
ユニットが個あった場合、そのボルツマンマシンは上の確率分布になっている。 と書いたが、その分布は具体的にどう決めるのだろうか?
物理ではよく「エネルギーが低いと安定する」と言われるけれど、その概念を持ち出してくる。 「安定する」というのを、「確率が高い」と読み替える。 とにかく、ボルツマンマシンの確率分布はエネルギーを用いて定義する。
ここで、エネルギー関数を考える。これは、状態が決まればエネルギーの大きさが決まる関数のこと。 今は物理をやっているわけではないのでエネルギーの単位は考えない。大きさだけ分かればいい。
このエネルギーに応じて、ある状態にある確率が
であるとする。つまり、
であるとする。この確率分布はBoltzmann分布と呼ばれて*1、統計力学でよく目にする。
見てのとおり、エネルギーが大きいと確率が低いと言うのが分かる。
エネルギー関数と指数型分布族
上で書いたとおり、ある状態がボルツマン分布に従うならば、エネルギー関数を決めれば確率が求まると言うことが分かる。
ところで、指数型分布族は、
と言う形をしていた。もしエネルギー関数がパラメータに対して、
と言う形をしているならば、ボルツマン分布は指数型分布族(の元)になっている。
ボルツマンマシンの確率分布
ここで、各ユニットとユニット間に対して実数パラメータとを導入しよう。 これらを使ってエネルギー関数が、
とかけている場合、これは実数パラメータとに関する指数型分布族になっている。
ボルツマンマシンは、この確率分布に従う。
基本的なボルツマンマシンの使用方法は、次のとおり。
- 学習データからラベルに対応した実数パラメータとを学習する。
- サンプルがどのラベルに対応しているかを検定する。(=サンプルにラベル付けを行う。)
検定の方法については、ちょっと前に書いた
bocchi-talks-information.hatenablog.com
の方法を少し工夫するとできるのだけれども、 学習データからラベルに対応した実数パラメータとを学習するのは、結構骨が折れる。 次回はこのボルツマンマシンの学習について書きたいと思う。
*1:Gibbs分布とも、Gibbs=Boltzmann分布とも呼ばれる。
準備中
学会予稿やら、博士論文の最終稿やらでここ数日ネタを考えてませんでした。
そしたら、その間に
内容そのものは
と言う話がhotになってきたので、次回から少し脱線して
ボルツマンマシンや、ディープラーニング、 最終的にこの稿のダイナミックボルツマンマシンの解説でもしようと考え中。
乞うご期待。
不偏推定量について
今日は、推定理論にもどって、不偏推定量について書こうと思う。
設定
データ集合が与えられているとして、を集合上の確率分布全体のなす集合とする。 また、モデルとして、 を仮定する。
同時独立分布
今回も、データが出てくる順番に関係なく毎回モデル上の分布にしたがって出てくるとする。 つまり、データ列が確率変数であり、各についてが確率分布に従っているとする。 このとき、データ列の具体値が得られる確率は
となっている。この性質を持つ確率分布を同時独立(independent and identically distributed; i.i.d.)分布とよぶ。
推定量(復習)
データ列からパラメータを導出する関数
を推定量と呼ぶのだった。
推定量はパラメータに値を持っていれば何でもいいから、ほとんど意味のない推定量もある。 ここで、意味のあるとは、「本当のパラメータを推定できる」と言う意味合い。
期待値(記号の導入)
では、意味のある推定量を導入する為に期待値と言うものを定義する。
関数と上の確率分布があるときに、期待値を
と定義する。また、分布がであった場合に
と書くこととする。また、データ列の確率分布がからなるi.i.d.分布だった場合、つまり、
だった場合、関数の期待値を
と書くこととする。
不偏推定量
では、意味のある推定量(のクラス)を定義する。 意味のあるとは、「本当のパラメータを推定できる」という意味合いだったから、
を満たす推定量なら、よさそう。意味合いとしては、どんなパラメータが真のパラメータであっても、 期待値の意味できちんとを当てることができる推定量。このような推定量を(大域)不偏推定量とよぶ*1。
ぱっと見たとこで分かるように、この不偏推定量はかなりきつい制限だ。 実際、モデルによっては、不偏推定量が存在しないこともある。
不偏推定量の中でも良いものを
ここでは、不偏推定量があることを前提に話をする。 このとき、推定量の誤差がもっとも小さくなるものが良い推定量と言えそう。
ここで、誤差として平均二乗誤差ををもってくると、
と分散と同じになっていることが分かる。(分散はで表すことにする。)
この分散をつかって、最も良い不偏推定量は
と定義できる。この最も良い不偏推定量は一様最小分散不偏推定量(Uniformly Minimum Variance Unbiased estimator; UMVU)とよばれる。
本当にそんなものあるの?
不偏推定量でもきつい条件だったのに、UMVUにいたってはもっと厳しい条件になっている。 さすがにそんな都合の良いものは、なさそうという感じはするけれど、いくつかのモデルでは、その存在を示すことができる。
また、最尤推定量の時にも少し書いたけれど、最尤推定量はデータの量が十分に大きいときUMVUに相当する性質を満たすことが知られている。 この性質が漸近有効性とよばれる。
これらのことについて、後日書こうと思う。
*1:全部のパラメータに対してよいと言う条件をかなり緩めて、「あるパラメータの近傍だけ不偏性をみたす」局所不偏推定量という概念もある。