多層ニューラルネットを試す

はじめに

FeedForwardNeuralNetwork。プロフェッショナルな「深層学習」本のバックプロパゲーションの導出が丁寧にされていてわかりやすかったので、それに合わせて書いてみる。

各層の活性化関数はロジスティック(シグモイド)関数、出力層の活性化関数はソフトマックス関数、誤差関数は交差エントロピー

コード

1インスタンスごとに重みを更新(SGD)。
直近TERM個のインスタンスの誤差平均が終了条件を満たしたら終了。
学習が収束してくれているので大丈夫そう。

#include <iostream>
#include <vector>
#include <cstdio>
#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;
}


struct UnitW {
  double u; //ユニットへの入力
  double z; //ユニットの出力
  double delta; //ユニットでのδ_j^l = ∂E_n / ∂u_j^(l)
  std::vector<double> w; //このユニットから次の層のユニットへの重み
  UnitW(int num_next_layer_node):w(num_next_layer_node, 0){
    for(int i=0; i<w.size(); i++){
      w[i] = normal_rand(0.0, 0.1);
    }
  }
};


class MultiLayerNeuralNetwork {
  double eps; //学習率
  std::vector< std::vector<UnitW> > network;

  void init_network(const std::vector<int>& num_layer_node){
    for(int i=0; i<num_layer_node.size(); i++){
      network.push_back( std::vector<UnitW>() );

      //(num_layer_node[i])番目をバイアスとして使うのですべての層で+1用意
      for(int j=0; j<num_layer_node[i] + 1; j++){
        if(i < num_layer_node.size()-1){ //入力層、隠れ層
          network[i].push_back(UnitW(num_layer_node[i+1]));
          
          if(j == num_layer_node[i]){ //バイアスの重みは0
            for(int k=0; k<network[i][j].w.size(); k++){
              network[i][j].w[k] = 0.0;
            }
          }
        }else{ //出力層
          network[num_layer_node.size()-1].push_back(UnitW(0));
        }
      }
    }
  }

public:
  MultiLayerNeuralNetwork(double eps, const std::vector<int>& num_layer_node):eps(eps){
    init_network(num_layer_node);
  }

  std::vector<double> forward_propagation(const std::vector<double>& in){
    for(int i=0; i<network.size(); i++){
      for(int j=0; j<network[i].size(); j++){
        network[i][j].u = 0.0;
      }
    }

    //入力層
    for(int j=0; j<network[0].size()-1; j++){
      network[0][j].z = in[j];
    }
    network[0][network[0].size()-1].z = 1.0;   

    //隠れ層と出力層
    for(int i=0; i<network.size()-1; i++){
      for(int j=0; j<network[i].size(); j++){
        //現在の層の出力zと重みを合わせて次の層の入力に渡す
        for(int k=0; k<network[i+1].size()-1; k++){
          network[i+1][k].u += network[i][j].z * network[i][j].w[k];
        }
      }
      //次の層の出力zを計算
      if(i < network.size()-2){ //隠れ層
        for(int j=0; j<network[i+1].size(); j++){
          network[i+1][j].z = 1.0 / (1.0 + exp(-network[i+1][j].u));
        }
        network[i+1][network[i+1].size()-1].z = 1.0; //バイアスは常に出力は1
      }else{ //出力層
        double Z = 0.0;
        for(int j=0; j<network[i+1].size()-1; j++){
          Z += exp(network[i+1][j].u);
        }
        for(int j=0; j<network[i+1].size()-1; j++){
          network[i+1][j].z = exp(network[i+1][j].u) / Z;
        }
      }
    }

    std::vector<double> ret;
    for(int i=0; i<network[network.size()-1].size()-1; i++){
      ret.push_back(network[network.size()-1][i].z);
    }
    return ret;
  }

  double back_propagation(const std::vector<double>& in, const std::vector<double>& d){
    double err = 0.0;
    for(int i=0; i<network.size(); i++){
      for(int j=0; j<network[i].size(); j++){
        network[i][j].delta = 0.0;
      }
    }

    //現在のネットワークでの結果を計算
    std::vector<double> y = forward_propagation(in);

    //出力層のdelta値の計算
    for(int i=0; i<network[network.size()-1].size()-1; i++){
      network[network.size()-1][i].delta = y[i] - d[i];
      err += - d[i] * log(y[i]);
    }

    //各ユニットのdelta値の計算
    for(int i=network.size()-2; i>=0; i--){
      for(int j=0; j<network[i].size(); j++){
        for(int k=0; k<network[i+1].size()-1; k++){
          double fu = 1.0 / (1.0 + exp(-network[i][j].u));
          double fdash = fu * (1.0 - fu);
          network[i][j].delta += network[i+1][k].delta * network[i][j].w[k] * fdash;
        }
      }
    }
    
    //各ユニットのdelta値を使って各重みにおける微分を計算し、SGDで重みを更新
    for(int i=network.size()-2; i>=0; i--){
      for(int j=0; j<network[i].size(); j++){
        for(int k=0; k<network[i+1].size()-1; k++){
          network[i][j].w[k] -= eps * (network[i+1][k].delta * network[i][j].z);
        }
      }
    }
    return err;
  }
};


int main(){
  const int TERM = 500; //TERM個単位でerrを確認
  const double finish_err = 0.01; //errがfinish_err未満になったら学習終了


  int layerN, trainN, testN;
  double eps;
  std::cin >> layerN;

  //各層のユニット数
  std::vector<int> num_layer_node;
  for(int i=0; i<layerN; i++){
    int num;
    std::cin >> num;
    num_layer_node.push_back(num);
  }
  int inN = num_layer_node[0];
  int outN = num_layer_node[num_layer_node.size()-1];

  std::cin >> eps;

  /// 学習 ////////////////////
  std::cin >> trainN;
  std::vector< std::vector<double> > out, in;
  for(int i=0; i<trainN; i++){
    std::cerr << "\r" << i;
    std::vector<double> out_one(outN, 0), in_one;
    int t;
    std::cin >> t;
    out_one[t] = 1;
    out.push_back(out_one);
    for(int j=0; j<inN; j++){
      double d;
      std::cin >> d;
      in_one.push_back(d);
    }
    in.push_back(in_one);
  }
  std::cerr << std::endl;

  //データシャッフル用
  std::vector<int> rnd;
  for(int i=0; i<trainN; i++){
    rnd.push_back(i);
  }
  std::random_shuffle(rnd.begin(), rnd.end());


  //学習ループ
  MultiLayerNeuralNetwork nn(eps, num_layer_node);
  int iter = 0;
  double err = 0.0;
  while(true){
    //TERM個単位で確認
    if(iter != 0 && iter%TERM == 0){
      err /= TERM;
      std::cerr << "err = " << err << std::endl;
      if(err < finish_err) break;
      err = 0;
    }

    err += nn.back_propagation(in[rnd[iter%trainN]], out[rnd[iter%trainN]]);
    iter++;
  }

  /// テスト ////////////////////
  std::cin >> testN;
  int match = 0;
  for(int i=0; i<testN; i++){
    std::vector<double> in_one;
    int out_t;
    std::cin >> out_t;

    for(int j=0; j<inN; j++){
      double d;
      std::cin >> d;
      in_one.push_back(d);
    }

    std::vector<double> res = nn.forward_propagation(in_one);
    double res_mx = 0;
    int res_i = 0;
    for(int j=0; j<res.size(); j++){
      if(res[j]>res_mx){
        res_mx = res[j];
        res_i = j;
      }
    }

    if(out_t == res_i) match++;
    std::cout << out_t << "\t" << res_i << "\t" << res_mx << std::endl;
  }

  //Accuracyの出力
  std::cout << "Acc = " << match/(double)testN << std::endl;

  return 0;
}

入力ファイルの形式

[layer数]
[入力層のユニット数]  [隠れ層1層目のユニット数]  ...  [隠れ層k層目のユニット数]  [出力層のユニット数]
[学習率]
[学習インスタンス数]
[出力]  [次元1の値]  [次元2の値] ...
...
テストインスタンス数
[出力]  [次元1の値]  [次元2の値] ...
...

出力は、0〜出力ユニット数-1の値。入力の次元は、入力層のユニット数と同じ事が必要。

結果

隠れ層1層のケースで確認。

XORパターン
  • 入力
$ cat xor.txt
3
2 2 2
0.01
4
0  0 0
1  0 1
1  1 0
0  1 1
4
0  0 0
1  0 1
1  1 0
0  1 1
  • 出力
$ ./a.out < xor.txt
3
err = 0.697136
...
err = 0.00992856
0	0	0.992468
1	1	0.991095
1	1	0.991083
0	0	0.986047
Acc = 1
MNISTの手書き数字認識

scikit_learn_dataのやつ。

  • 入力
$ cat mnist.txt
3
784 100 10
0.01
60000
0  0.0 0.0 ...(省略)
...(省略)
10000
0  0.0 0.0 ...(省略)
...(省略)
  • 出力
$ ./a.out < mnist.txt
59999
err = 2.28309
err = 1.34394
err = 1.08896
...
Acc = 0.9776
  • 収束までにかかる時間

CPU(i7-4790)計算で、結果でるまで16分30秒ぐらい。
コードが違うけど、chainerだとGPU使って30秒程度で終わる、、、やっぱりchainerだな!

参考