行列演算でニューラルネット

巷では何でもできると評判のニューラルネットワーク
(少なくとも身内の中では)
でも学習に時間がかかる、局所解に陥るなどSVMと比べると欠点も多くて、最近不人気みたい。

ニューラルネットワークは万能だと洗脳されている僕にとっては、もっと人気が出てほしいのでソースコードを晒しておく。

とりあえず、サンプルとしてXOR問題を解くプログラムで、変数名はPRMLに合わせてある。

main.cpp

int main() 
{
	// 入力
	VectorXd x[4];
	for (int i = 0; i < 4; ++i) {
		x[i] = VectorXd::Zero(2);
	}
	x[0] << 0.0, 0.0;
	x[1] << 0.0, 1.0;
	x[2] << 1.0, 0.0;
	x[3] << 1.0, 1.0;

        // 教師信号
	VectorXd t[4];
	for (int i = 0; i < 4; ++i) {
		t[i] = VectorXd::Zero(1);
	}
	t[0] << 0.0;
	t[1] << 1.0;
	t[2] << 1.0;
	t[3] << 0.0;

	MatrixXd w0 = MatrixXd::Random(kNumHidden, 1 + kNumInput);
	MatrixXd w1 = MatrixXd::Random(kNumOutput, 1 + kNumHidden);

        // 学習回数300回
	for (int n = 0; n < 300; ++n) {
		for (int i = 0; i < 4; ++i) {
			learn(x[i], t[i], w0, w1);
		}
	}

        // 出力結果
	for (int i = 0; i < 4; ++i) {
		std::cout << feedFoward(x[i], w0, w1) << std::endl;
	}

	//std::cout << "w0\n" << w0 << std::endl;
	//std::cout << "w1\n" << w1 << std::endl;

	return 0;
}

main.cppのニューラルネット

#include <Eigen/Dense>
using namespace Eigen;

// 活性化関数
//auto lambda_act = [](const double x) { return 1.7159*tanh(0.6667*x); };
//auto lambda_act_p = [](const double x) { return 1.7159*0.6667*(1.0 - 1.0/(1.7159*1.7159)*x*x); };

auto lambda_act = [](const double x) { return tanh(x); };
auto lambda_act_p = [](const double x) { return 1.0 - x*x; };

//auto lambda_act = [](const double x) { return 1.0 / (1+exp(-x)); };
//auto lambda_act_p = [](const double x) { return x*(1.0 - x); };

const int kNumInput = 2; // 入力数
const int kNumHidden = 3; // 隠れ層の大きさ
const int kNumOutput = 1; // 出力数
const double kEta = 0.3; // 学習係数

// softmax関数
void Softmax(VectorXd& in)
{
	VectorXd vec_exp = in.array().exp();
	const double sum_exp = vec_exp.sum();

	in = vec_exp.array() / sum_exp;
}

// 学習用
// 2-layer NN
void learn(const VectorXd& in, const VectorXd& tk, MatrixXd& w0, MatrixXd& w1)
{
	// Feedfoward
	VectorXd xi(1 + kNumInput);
	xi(0) = 1.0;
	xi.tail(kNumInput) = in; 
	VectorXd aj = w0 * xi;
  
	VectorXd zj(1 + kNumHidden);
	zj(0) = 1.0;
	zj.tail(kNumHidden) = aj.unaryExpr(lambda_act);

	VectorXd yk = w1 * zj;

	//Softmax(yk);

	// Error back propagate
	VectorXd delta_k = yk - tk;
	VectorXd delta_j = w1.transpose() * delta_k;

	delta_j = delta_j.array() * zj.unaryExpr(lambda_act_p).array();

	w1.array() -= kEta * (delta_k * zj.transpose()).array(); // TODO noalias()付ける
	w0.array() -= kEta * (delta_j.tail(kNumHidden) * xi.transpose()).array();
}

// 識別用
// learn()の上半分と同じ内容
VectorXd feedFoward(const VectorXd& in, const MatrixXd& w0, const MatrixXd& w1)
{
	VectorXd xi(1 + kNumInput);
	xi(0) = 1.0;
	xi.tail(kNumInput) = in; 

	VectorXd aj = w0 * xi;
	VectorXd zj(1 + kNumHidden);
	zj(0) = 1.0;
	zj.tail(kNumHidden) = aj.unaryExpr(lambda_act);

	VectorXd yk = w1 * zj;

	//Softmax(yk);

	return yk;
}

主力結果が

0
1
1
0

となれば学習成功。
行列演算を使って実装しているのでコードの見通しもいいし、謎の行列積アルゴリズムのおかげで速いと思う。(未確認)
ただ学習係数が固定なので可変にした方がいいし、weightの保存ができないので、実際使おうとすると面倒くさい。
学習係数に関しては自動的に値を更新する、iRPROP-のコードがあるので載せておく。(覚えていないので間違っているかも)
weight.h

#include <random>
#include <time.h>
#include <Eigen/Dense>
using namespace Eigen;

static const double kEta = 0.0025; // for sgm

static const double kEta0 = 0.0125;
static const double kEtaPlus = 1.2;
static const double kEtaNeg = 0.5;
static const double kEtaMax = 50;
static const double kEtaMin = 0.000001;

class Weight {
public:
	Weight(int rows, int cols) : rows_(rows), cols_(cols)
	{
		std::mt19937 engine( static_cast<unsigned long>(time(0)) );	
		std::uniform_real_distribution<double> dist(-0.005, 0.005);
		
		weight_ = MatrixXd::Zero(rows_, cols_);
		weight_ = weight_.unaryExpr([&](const double x) { return dist(engine); });

		pre_weight_ = weight_;
		pre_deriv_weight_ = MatrixXd::Zero(rows_, cols_);
		deriv_weight_ = MatrixXd::Zero(rows_, cols_);
		pre_step_size_ = MatrixXd::Constant(rows_, cols_, kEta0);
                step_size_ = pre_step_size;
	}

	const MatrixXd& weight() const {
		return weight_;
	}

	MatrixXd& deriv_weight() {
		return deriv_weight_;
	}

	void SGM() {
		weight_ -= kEta * deriv_weight_;
		deriv_weight_.setZero();
	}

	// iRprop-
	void iRprop_neg() {
		for ( int y = 0; y < rows_; ++y ) {
			for ( int x = 0; x < cols_; ++x ) {
				double has_sign_changed = pre_deriv_weight_(y, x) * deriv_weight_(y, x);

				if ( has_sign_changed > 0.0 ) {
					step_size_(y, x) = std::min(pre_step_size_(y, x)*kEtaPlus, kEtaMax);
				} else if ( has_sign_changed < 0.0 ) {
					step_size_(y, x) = std::max(pre_step_size_(y, x)*kEtaNeg, kEtaMin);
					deriv_weight_(y, x) = 0.0;
				}
				weight_(y, x) = pre_weight_(y, x) - sign(deriv_weight_(y, x)) * step_size_(y,x);
			}
		}

		pre_deriv_weight_ = deriv_weight_;
		pre_weight_ = weight_;
		pre_step_size_ = step_size_;
		deriv_weight_.setZero();
	}

private:
	const int rows_;
	const int cols_;
	MatrixXd pre_weight_;
	MatrixXd pre_deriv_weight_;	
	MatrixXd pre_step_size_;
	MatrixXd weight_;
	MatrixXd step_size_;
	MatrixXd deriv_weight_;

	double sign(const double x) {
		if ( x > 0.0 ) {
			return 1.0;
		} else if ( x < 0.0 ) {
			return -1.0;
		} else {
			return 0.0;
		}
	}
};