TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

Caffeを使ってC++で3層パーセプトロンを学習する

前回の日記でCaffeをC++から使うことができたので、3層パーセプトロンを学習させてみた。

f:id:TadaoYamaoka:20160626210549p:plain

Caffeではモデルをprototxt形式で記述する。
学習方法を記述したsolver.prototxtとネットワークを記述したnet.prototxtの2つのファイルを作成する。
ファイル名は別の名前でもよい。

3層パーセプトロンを学習する場合は以下のように記述する。

solver.prototxt
net: "net.prototxt"
type: "RMSProp"
base_lr: 0.01
lr_policy: "step"
gamma: 0.1
stepsize: 1000
max_iter: 1000
display: 10
net.prototxt
name: "MultilayerPerceptrons"
layer {
  name: "input"
  type: "MemoryData"
  top: "input"
  top: "label"
  memory_data_param {
    batch_size: 10
    channels: 2
    height: 1
    width: 1
  }
}
layer {
  name: "hidden1"
  type: "InnerProduct"
  bottom: "input"
  top: "hidden1"
  inner_product_param {
    num_output: 3
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}
layer {
  name: "relu1"
  type: "ReLU"
  bottom: "hidden1"
  top: "hidden1"
}
layer {
  name: "hidden2"
  type: "InnerProduct"
  bottom: "hidden1"
  top: "hidden2"
  inner_product_param {
    num_output: 2
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}
layer {
  name: "loss"
  type: "SoftmaxWithLoss"
  bottom: "hidden2"
  bottom: "label"
  top: "loss"
  include {
    phase: TRAIN
  }
}
layer {
  name: "prob"
  type: "Softmax"
  bottom: "hidden2"
  top: "prob"
  include {
    phase: TEST
  }
}

記述方法の詳細は、Caffeの公式ページのSolverLayersを参考にしてほしい。

入力層は、C++のプログラムからデータを与えるため、

type: "MemoryData"

としている。

出力層は、

  include {
    phase: TRAIN
  }

  include {
    phase: TEST
  }

を記述したものの2つあるが、学習用と予測用で分けているためである。
学習用と予測用を別ファイルにしてもよい。


続いてC++のプログラムについて解説する。

学習

初期化
	FLAGS_alsologtostderr = 1;
	GlobalInit(&argc, &argv);

	Caffe::set_mode(Caffe::CPU);

初期化とモードの設定を行う。この場合はCPUモードとしている。

prototxtを読み込んでSolverを作成する
	SolverParameter solver_param;
	ReadProtoFromTextFileOrDie("solver.prototxt", &solver_param);
	std::shared_ptr<Solver<float>>
		solver(SolverRegistry<float>::CreateSolver(solver_param));
	const auto net = solver->net();
入力データを設定する
	const int data_size = 100;
	const int input_num = 2;
	float input_data[data_size][input_num] = {
		{ 0.18, 0.15 }, { 0.20, 0.90 }, { 0.94, 0.11 }, ・・・(略) };
	float label[data_size] = { 1, 0, 0, ・・・(略) };
	const auto input_layer =
		boost::dynamic_pointer_cast<MemoryDataLayer<float>>(
		net->layer_by_name("input"));
	input_layer->Reset((float*)input_data, (float*)label, data_size);
学習する
	solver->Solve();

学習結果は、「_iter_1000.caffemodel」に保存される。1000の部分はsolver.prototxtに記述したイテレーション回数(max_iter)になる。

予測

初期化
	Net<float> net_test("net.prototxt", TEST);
	net_test.CopyTrainedLayersFrom("_iter_1000.caffemodel");

ネットワーク定義とモデルパラメータを読み込む。引数をTESTにすることで、phase: TESTとしたlayerが有効になる。

予測する
	const auto input_test_layer =
		boost::dynamic_pointer_cast<MemoryDataLayer<float>>(
		net_test.layer_by_name("input"));
	for (int batch = 0; batch < 10; batch++)
	{
		input_test_layer->Reset((float*)input_data + batch * 20, (float*)label + batch * 10, 10);

		const auto result = net_test.Forward();

		const auto data = result[1]->cpu_data();
		for (int i = 0; i < 10; i++)
		{
			LOG(INFO) << data[i * 2] << ", " << data[i * 2 + 1] << ", " << (data[i * 2] < data[i * 2 + 1]) ? 0 : 1;
		}
	}

学習では、入力データは複数ミニバッチのデータをまとめて入力できたが、予測では、ミニバッチサイズ分のデータを入力とする。
MemoryDataLayerはラベルが必須のためダミーのデータを与えている。

入力データを入力層に設定した後、「Forward()」で順伝播を実行し、結果を戻り値で受け取る。
結果のvectorの1番目の要素は、ダミーで与えたラベルになっているので無視する。
2番目の要素がこの例では、softmaxの出力になっている。

実行結果

入力データとして、x1とx2が0.7未満の場合にクラス0、それ以外がクラス1となる、非線形な識別面のデータを学習させた。

f:id:TadaoYamaoka:20160626213549p:plain


学習を行った結果、以下のグラフとなった。正解率は96%である。

f:id:TadaoYamaoka:20160626213416p:plain

非線形な識別面が正しく学習できている。
(サンプルのため、学習データと同一のデータを与えているので、汎化能力については検証していない。)

学習方法は、RMSPropとした場合に一番うまくいった。

なお、SGDの場合は、識別面がほぼ直線となり正しく学習できなかった。

f:id:TadaoYamaoka:20160626214044p:plain


Visual Studio 2013のプロジェクトをGithubに公開しているので参考にしてほしい。

github.com

補足

普通にCaffeで学習させる場合は、caffeコマンドを使った方が簡単です。
予測を行う場合は、pycaffeを使う方が簡単です。
CaffeをC++のプログラムに組み込んで使いたい方向けの説明です。


Caffeのprototxtの記述方法については、以下の本を参考にしました。

www.oreilly.co.jp