GP-MIで遊ぶ

はじめに

http://live.nicovideo.jp/watch/lv228162988
先週のNL研のニコ生で、ベイズ的最適化についての招待講演を見ていて「SEは滑らかすぎる」という発言がよくわからなかったので、GP-MIを試してみる。


Contal et al., Gaussian Process Optimization with Mutual Information
http://arxiv.org/abs/1311.4825
http://econtal.perso.math.cnrs.fr/presentations/slides_icml14.pdf

コード

カーネルは、通常(?)のSquared Exponential kernel。
逆行列求めるところが重いのでEigen使う版も一緒に。
いつものごとく雑。

#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <algorithm>
#include <cmath>

static const double PI = 3.14159265358979323846264338;

//xorshift
// 注意: longではなくint(32bit)にすべき
unsigned long xor128(){
  static unsigned long x=123456789, y=362436069, z=521288629, w=88675123;
  unsigned long t;
  t=(x^(x<<11));
  x=y; y=z; z=w;
  return w=(w^(w>>19))^(t^(t>>8));
}
//[0,1)の一様乱数
double frand(){
  return xor128()%1000000/static_cast<double>(1000000); 
}
//正規乱数
double normal_rand(double mu, double sigma2){
  double sigma = sqrt(sigma2);
  double u1 = frand(), u2 = frand();
  double z1 = sqrt(-2*log(u1)) * cos(2*PI*u2);
  //double z2 = sqrt(-2*log(u1)) * sin(2*PI*u2);
  return mu + sigma*z1;
}

#ifdef USE_EIGEN
#include <Eigen/Core>
#include <Eigen/LU>
using namespace Eigen;
#else
//行列計算用
class Matrix {
  int m, n;
  std::vector<double> val;

  void splice(int i, int j){
    int N = val.size();
    if(i+(j-1)<N){
      std::vector<double> vl(N-j, 0.0);
      int vl_idx = 0, val_idx = 0;
      while(val_idx<N){
        if(val_idx<i||i+j-1<val_idx){
          vl[vl_idx] = val[val_idx];
          vl_idx++;
        }
        val_idx++;
      }
      val = vl;
    }
  }
  Matrix clone(){
    Matrix ret(m, n);
    for(int i=0; i<m; i++){
      for(int j=0; j<n; j++){
        ret.setVal(i, j, val[n*i+j]);
      }
    }
    return ret;
  }

public:
  Matrix():m(0),n(0){}
  Matrix(int m, int n):m(m),n(n){
    if(m>0 && n>0){
      for(int i=0; i<m*n; i++){
        val.push_back(0.0);
      }
    }
  }
  Matrix(const Matrix& mat){
    m = mat.m;
    n = mat.n;
    val = mat.val;
  }

  int getM() const { return m; }
  int getN() const { return n; }
  double getVal(int i, int j) const { return val[n*i+j]; }
  void setVal(int i, int j, double x){ val[n*i+j] = x; }
  bool isSquare(){ return n==m; }

  Matrix add(const Matrix& mat){
    if(m == mat.m && n == mat.n){
      Matrix ret(m, n);
      for(int i=0; i<m; i++){
        for(int j=0; j<n; j++){
          ret.setVal(i, j, val[n*i+j] + mat.getVal(i,j));
        }
      }
      return ret;
    }
    return Matrix(-1, -1);
  }
  Matrix operator+(const Matrix& mat){ return add(mat); }

  Matrix sub(const Matrix& mat){
    if(m == mat.m && n == mat.n){
      Matrix ret(m, n);
      for(int i=0; i<m; i++){
        for(int j=0; j<n; j++){
          ret.setVal(i, j, val[n*i+j] - mat.getVal(i,j));
        }
      }
      return ret;
    }
    return Matrix(-1, -1);
  }
  Matrix operator-(const Matrix& mat){ return sub(mat); }

  Matrix prod(const Matrix& mat){
    if(n == 1 && mat.n == 1 && m == mat.m){
      Matrix ret(m, 1);
      for(int i=0; i<m; i++){
        ret.setVal(i, 0, getVal(i, 0) * mat.getVal(i, 0));
      }
      return ret;
    }
    else if(n == mat.m){
      Matrix ret(m, mat.n);
      for(int i=0; i<m; i++){
        for(int j=0; j<mat.n; j++){
          double d = 0.0;
          for(int k=0; k<n; k++){
            d += val[n*i+k] * mat.getVal(k,j);
          }
          ret.setVal(i, j, d);
        }
      }
      return ret;
    }
    return Matrix(-1, -1);
  }
  Matrix operator*(const Matrix& mat){ return prod(mat); }

  void time(double x){
    for(int i=0; i<m; i++){
      for(int j=0; j<n; j++){
        val[n*i+j] *= x;
      }
    }
  }

  Matrix transpose(){
    Matrix ret(n, m);
    for(int i=0; i<m; i++){
      for(int j=0; j<n; j++){
        ret.setVal(j, i, val[n*i+j]);
      }
    }
    return ret;
  }
  
  double cofactor(int i, int j){
    Matrix mat = clone();
    mat.splice(i*mat.n, mat.m);
    mat.m -= 1;
    for(int k=mat.m-1; k>=0; k--){
      mat.splice(k*mat.n+j, 1);
    }
    mat.n -= 1;
    return mat.det() * ( ((i+j+2)%2==0) ? 1 : -1);
  }
  double det(){
    if(isSquare()){
      if(m == 2){
        return val[0]*val[3]-val[1]*val[2];
      }else{
        double d = 0;
        for(int k=0; k<n; k++){
          d += val[k] * cofactor(0, k);
        }
        return d;
      }
    }else{
      return 0.0;
    }
  }
  Matrix cofactorMatrix(){
    Matrix mat(m, n);
    for(int i=0; i<m; i++){
      for(int j=0; j<n; j++){
        mat.setVal(j, i, cofactor(i, j));
      }
    }
    return mat;
  }
  Matrix inverse(){
    if(isSquare()){
      double d = det();
      if(d != 0){
        Matrix mat;
        if(m>2){
          mat = cofactorMatrix();
        } else {
          mat = Matrix(2, 2);
          mat.setVal(0, 0, val[3]);
          mat.setVal(0, 1, -val[1]);
          mat.setVal(1, 0, -val[2]);
          mat.setVal(1, 1, val[0]);
        }
        mat.time(1 / d);
        return mat;
      }
    }else{
      return Matrix(-1, -1);
    }
    return Matrix(-1, -1);
  }
};
std::ostream& operator<<(std::ostream& os, const Matrix& mat){
  for(int i=0; i<mat.getM(); i++){
    for(int j=0; j<mat.getN(); j++){
      os << mat.getVal(i,j) << " ";
    }
    os << std::endl;
  }
  return os;
}
#endif



class GP_MI {  
  double d; //xの次元
  double alpha; //活用探索のトレードオフ調整用パラメータ
  double noise_sigma2; //ノイズの分散
  int ITER; //サンプリング回数
  double gamma;

  bool flg_obs; //推定,観測を繰り返しているかどうかの確認用フラグ

  //観測データ
  std::vector< std::vector<double> > obs_x;
  std::vector<double> obs_y;

  //各x_iの探索範囲
  std::vector<double> x_min, x_max;

  //kernel(x,x')の値を求める
  double calc_kernel(const std::vector<double>& x1, const std::vector<double>& x2){
    //Squared exponential kernel
    double sigma2 = 1.0;
    double l2 = 0.01; //比較的ガタガタを試すので小さめ

    double z = 0.0;
    for(size_t i=0; i<x1.size(); i++){
      z += pow(((x1[i] - x2[i])), 2.0) / (2.0 * l2);
    }
    return sigma2 * exp(-z);
  }

public:
  GP_MI(int d, double alpha, double noise_sigma2, int ITER):
    d(d),
    alpha(alpha),
    noise_sigma2(noise_sigma2),
    gamma(0.0),
    x_min(d, 0.0),
    x_max(d, 1.0),
    ITER(ITER),
    flg_obs(false)
  { }

  //xの探索範囲を指定
  void set_x_range(int di, double mn, double mx){
    x_min[di] = mn;
    x_max[di] = mx;
  }

  //現在までの観測結果から推定される関数をもとに、一番探した方がよいxを返す
  std::vector<double> get_best_x(){
    //前回の観測が行われたかチェック
    if(!flg_obs){
      std::cerr << "please observe and set" << std::endl;
      return std::vector<double>();
    }
    flg_obs = false;
    
    //前計算C_T, Y_T
#ifdef USE_EIGEN
    MatrixXd C_T = MatrixXd::Zero(obs_y.size(), obs_y.size());
    MatrixXd Y_T = MatrixXd::Zero(obs_y.size(), 1);
    for(size_t i=0; i<obs_y.size(); i++){
      for(size_t j=0; j<obs_y.size(); j++){
        C_T(i,j) = calc_kernel(obs_x[i], obs_x[j]) + (i==j?noise_sigma2:0.0);
      }
    }
    for(size_t i=0; i<obs_y.size(); i++){
      Y_T(i,0) = obs_y[i];
    }
    MatrixXd C_T_inv = C_T.inverse();
#else
    Matrix C_T(obs_y.size(), obs_y.size());
    Matrix Y_T(obs_y.size(), 1);
    for(size_t i=0; i<obs_y.size(); i++){
      for(size_t j=0; j<obs_y.size(); j++){
        C_T.setVal(i,j, calc_kernel(obs_x[i], obs_x[j]) + (i==j?noise_sigma2:0.0) );
      }
    }
    for(size_t i=0; i<obs_y.size(); i++){
      Y_T.setVal(i,0, obs_y[i]);
    }
    Matrix C_T_inv = C_T.inverse();
#endif

    //ITER回サンプリングしてargmaxを求める
    double best_a = -1.0e10;
    std::vector<double> best_x;
    double best_sigma2 = 0;

    for(size_t iter=0; iter<ITER; iter++){
      //適当なxを生成
      std::vector<double> x(d);
      for(size_t i=0; i<d; i++) x[i] = (x_max[i]-x_min[i])*frand() + x_min[i];
      
#ifdef USE_EIGEN
      MatrixXd kT = MatrixXd::Zero(obs_y.size(), 1);
      for(size_t i=0; i<obs_y.size(); i++){
        kT(i,0) = calc_kernel(obs_x[i], x);
      }

      double kxx = calc_kernel(x, x);

      MatrixXd kCY = kT.transpose() * ( C_T_inv * Y_T );
      MatrixXd kCk = kT.transpose() * ( C_T_inv * kT );

      double mu = kCY(0,0);
      double sigma2 = kxx - kCk(0,0);
#else
      Matrix kT(obs_y.size(), 1);
      for(size_t i=0; i<obs_y.size(); i++){
        kT.setVal(i,0, calc_kernel(obs_x[i], x));
      }

      double kxx = calc_kernel(x, x);

      Matrix kCY = kT.transpose() * ( C_T_inv * Y_T );
      Matrix kCk = kT.transpose() * ( C_T_inv * kT );

      double mu = kCY.getVal(0,0);
      double sigma2 = kxx - kCk.getVal(0,0);
#endif

      //獲得関数の値を求める
      double phi = sqrt(alpha) * (sqrt(sigma2 + gamma) - sqrt(gamma));      
      if(best_a < mu + phi){
        best_a = mu + phi;
        best_x = x;
        best_sigma2 = sigma2;
      }
    }

    gamma += best_sigma2;
    return best_x;
  }

  //get_best_x()で得られたxに対する実際の関数の値を、観測結果として登録
  void set_observation(std::vector<double>& x, double y){
    obs_x.push_back(x);
    obs_y.push_back(y);
    flg_obs = true;
  }

};



//ブラックボックスな関数
double func(const std::vector<double>& x){
  return x[0] * sin(10.0 * x[0]);
}

int main(){
  //xの次元数  alpha  noise_sigma2  サンプリング回数
  GP_MI gpmi(1, 10.0, 0.0001, 500);

  //各次元の範囲
  gpmi.set_x_range(0, 0.0, 1.0);

  //最初に何点か求めておく
  std::vector<double> x(1);
  x[0] = 0;
  gpmi.set_observation(x, func(x));
  x[0] = 0.5;
  gpmi.set_observation(x, func(x));
  x[0] = 1.0;
  gpmi.set_observation(x, func(x));

  //自動で探索点を選ぶ
  for(int i=0; i<30; i++){
    std::vector<double> best_x = gpmi.get_best_x();
    std::cout << i << " : best_x = " << best_x[0] << std::endl;
    gpmi.set_observation(best_x, func(best_x));
  }

  return 0;
}

結果

最初に0,0.5,1.0の3点だけ求めた状態からスタート。
αがlog(2/δ), 0<δ<1とかだけど、小さいと隣の山に探しに行ってくれないので、大きいけど、α=10と50で試してみる。
グラフは、赤がμとσ^2、緑が獲得関数μ+φ。

x * sin(10.0 * x)

【α=10】

最適解は求められているけど、左の山が分散大きくても獲得関数の値が低いまま。
【α=50】

αを大きくする(大きくしすぎ?)と、左の山も探してくれてる。

x * cos(30.0 * x)

もっとガタガタしたもの。
【α=10】

うまくいっていない(本当の最適点はx=0.84ぐらい)。
動画で「滑らかすぎる」という発言があったけど、滑らかすぎて、関数の形の推定がうまくいかない=適切な探索点をみつけられない、ということか(?)。
ただ、ARDなSEカーネルやMaternカーネルの実装、最適なパラメータ探索(MCMC)がちょっとよくわかってない・・・


http://www.slideshare.net/issei_sato/deep-learning-41310617
> BOツール公開予定

ω・)待機。