多層ニューラルネットを試す
はじめに
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だな!
参考
- 岡谷, 機械学習プロフェッショナルシリーズ「深層学習」, 講談社
- http://aidiary.hatenablog.com/entry/20140205/1391601418
- 以前遊んだやつは、http://d.hatena.ne.jp/jetbead/20121110/1352525897