FeedforwardNeuralNetworkで遊ぶ

はじめに

最近再び注目を浴びているニューラルネットで遊んでみた。
簡単な3層の完全結合の階層型ニューラルネット(多層パーセプトロン)。

XOR

  • 2入力1出力で、xorの出力は線形分離可能ではない
  • これを学習してみて分離できてるか見てみる
コード
#include <iostream>
#include <fstream>
#include <vector>
#include <map>
#include <cmath>

//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)の一様乱数
// 注意: int_maxぐらいで割った方がよい
double frand(){
  return xor128()%1000000/static_cast<double>(1000000); 
}

//FeedfowardNeuralNetwork(多層パーセプトロン)
class NeuralNetwork {
  static const double nu = 0.8; //学習率
  static const double alpha = 0.75; //安定化定数

  int N_in, N_out, N_hide; //入力層、出力層、隠れ層のユニット数
  std::vector<double> output_in, output_out, output_hide; //入力層、出力層、隠れ層の出力値
  std::vector<double> h_hide, h_out; //隠れ層と出力層のしきい値
  std::vector< std::vector<double> > w_in_hide, w_hide_out; //入力層と隠れ層、隠れ層の出力層の結合重み

  //for backpropagation
  std::vector<double> dh_hide, dh_out; //隠れ層と出力層のしきい値の変化量
  std::vector< std::vector<double> > dw_in_hide, dw_hide_out; //入力層と隠れ層、隠れ層と出力層の結合重みの変化量

  double sigmoid(double x, double a){
    return 1.0 / (1.0 + exp(a - x));
  }
  double sigmoid_dash(double fx){
    return fx * (1.0 - fx);
  }
  //二乗和誤差
  double error_rate(const std::vector<double>& in, const std::vector<double>& out){
    std::vector<double> ret = forward_propagation(in);
    double res = 0.0;
    for(size_t i=0; i<ret.size(); i++){
      res += fabs(ret[i] - out[i]) * fabs(ret[i] - out[i]);
    }
    return res/ret.size();
  }


  void back_propagation(const std::vector<double>& in, const std::vector<double>& out){
    //学習信号
    std::vector<double> sig_out(N_out), sig_hide(N_hide);
    //現在の出力値を計算
    std::vector<double> ret = forward_propagation(in);

    //出力層からの学習信号
    for(size_t i=0; i<N_out; i++){
      sig_out[i] = (out[i] - ret[i]) * sigmoid_dash(ret[i]);
    }

    //「隠れ層 -> 出力層」の結合重みの学習
    for(size_t i=0; i<N_hide; i++){
      double sum = 0.0;
      for(size_t j=0; j<N_out; j++){
        dw_hide_out[i][j] = nu * sig_out[j] * output_hide[i] + alpha * dw_hide_out[i][j];
        w_hide_out[i][j] += dw_hide_out[i][j];
        sum += sig_out[j] * w_hide_out[i][j];
      }
      sig_hide[i] = sum * sigmoid_dash(output_hide[i]);
    }
    //「隠れ層 -> 出力層」のしきい値の学習
    for(size_t i=0; i<N_out; i++){
      dh_out[i] = nu * sig_out[i] + alpha * dh_out[i];
      h_out[i] += dh_out[i];
    }

    //「入力層 -> 隠れ層」の結合重みの学習
    for(size_t i=0; i<N_in; i++){
      for(size_t j=0; j<N_hide; j++){
        dw_in_hide[i][j] = nu * sig_hide[j] * output_in[i] + alpha * dw_in_hide[i][j];
        w_in_hide[i][j] += dw_in_hide[i][j];
      }
    }
    //「入力層 -> 隠れ層」のしきい値の学習
    for(size_t i=0; i<N_hide; i++){
      dh_hide[i] = nu * sig_hide[i] + alpha * dh_hide[i];
      h_hide[i] += dh_hide[i];
    }
  }

  
  std::vector<double> forward_propagation(const std::vector<double>& in){
    //入力層の出力値は入力そのまま
    for(size_t i=0; i<N_in; i++) output_in[i] = in[i];

    //入力層 -> 隠れ層
    for(size_t i=0; i<N_hide; i++){
      double sum = 0;
      for(size_t j=0; j<N_in; j++){
        sum += output_in[j] * w_in_hide[j][i];
      }
      output_hide[i] = sigmoid(sum + h_hide[i], 0.0);
    }

    //隠れ層 -> 出力層
    for(size_t i=0; i<N_out; i++){
      double sum = 0;
      for(size_t j=0; j<N_hide; j++){
        sum += output_hide[j] * w_hide_out[j][i];
      }
      output_out[i] = sigmoid(sum + h_out[i], 0.0);
    }
    return output_out;
  }

public:
  NeuralNetwork(int n_in, int n_out, int n_hide):
    N_in(n_in), N_out(n_out), N_hide(n_hide),
    output_in(n_in, 0.0), output_out(n_out, 0.0),
    output_hide(n_hide, 0.0),
    h_hide(n_hide, 0.0),
    h_out(n_out, 0.0),
    w_in_hide(n_in, std::vector<double>(n_hide, 0.0)),
    w_hide_out(n_hide, std::vector<double>(n_out, 0.0)),
    dh_hide(n_hide, 0.0),
    dh_out(n_out, 0.0),
    dw_in_hide(n_in, std::vector<double>(n_hide, 0.0)),
    dw_hide_out(n_hide, std::vector<double>(n_out, 0.0)){

    //重みを適当な値(-0.3~0.3)で初期化
    for(size_t i=0; i<n_in; i++){
      for(size_t j=0; j<n_hide; j++){
        w_in_hide[i][j] = frand() * 0.6 - 0.3;
      }
    }
    for(size_t i=0; i<n_hide; i++){
      for(size_t j=0; j<n_out; j++){
        w_hide_out[i][j] = frand() * 0.6 - 0.3;
      }
    }
  }

  //学習
  void train(const std::string& filename, int loop){
    int train_case;
    std::vector<double> in, out;

    for(int i=0; i<loop; i++){
      //std::cout << i << std::endl;
      std::ifstream ifs(filename.c_str());
      ifs >> train_case;

      for(int j=0; j<train_case; j++){
        double tmp;
        in.clear();
        out.clear();

        for(int k=0; k<N_in; k++){
          ifs >> tmp;
          in.push_back(tmp);
        }
        for(int k=0; k<N_out; k++){
          ifs >> tmp;
          out.push_back(tmp);
        }
        
        back_propagation(in, out);

        //std::cerr << error_rate(in, out) << std::endl;
      }
    }
  }

  //評価
  void test(){
    std::vector<double> v(N_in);
    while(std::cin >> v[0]){
      for(size_t i=1; i<N_in; i++) std::cin >> v[i];

      std::vector<double> ret = forward_propagation(v);
      for(size_t i=0; i<ret.size(); i++){
        std::cout << ret[i] << " ";
      }
      std::cout << std::endl;
    }    
  }

};

int main(){
  NeuralNetwork neural(2,1,2); //入力層のユニット数、出力層のユニット数、隠れ層のユニット数
  neural.train("train.in", 5000);
  neural.test();

  return 0;
}
学習データ
4
0 1
1
0 0
0
1 0
1
1 1
0
結果
$ ./a.out
0 0
0.0102233
1 0
0.991307
0 1
0.991315
1 1
0.00891325
  • 分離できてる
  • けど、学習データで「0 0」のケースを最初に持ってくるとうまく学習が収束しない、、

LIBSVMのデータ(a9a)

コード
  • 学習データとテストデータの形式を変換
  • 上記コードをファイルから読み込めるよう修正
結果
  • パラメータ(nu,alpha)とループ回数、学習データの順番を変えて何回か実行
  • 一番よかった結果
nu = 0.8
alpha = 0.75
ループ回数 = 30
Result: 13729 / 16281 (84.325287%)
  • 学習率nuが大きいと収束は速いが、局所解っぽいところに落ち着きやすい
    • この場合だと、結果が常に「-1」=評価データ中の出力が「-1」の個数12435回になりやすかった
    • ただし、学習データの順番をシャッフルすると、パラメータの値が同じでもよりよい結果がでた
  • 学習率nuを小さくして、学習回数を増やすと良い結果がでやすかった
  • いろんな条件で結果が大きく変わるので、難しい。。。
メモ
  • 学習データのシャッフルに使ったコマンド
$ tail +1 a9a.train.modify | perl -MList::Util=shuffle -e 'print shuffle(<>)'|sed -e '1s/^/32561\
/' > a9a.train.shuffle

参考文献