いつもの作業の備忘録

作業を忘れがちな自分のためのブログ

【Caffe】回帰問題を解く

0.課題

 GoogLeNetを回帰問題に応用する方法を紹介する。その例として、以下の図に示すような円の回帰を考える。中心座標(x,y)、半径d、色b、g、rの6つをランダムに決定して円を描画した224x224ピクセルの画像を入力し、6つの値x、y、d、b、g、rを回帰するモデルを作成する。示している図では、塗りつぶされた円が入力画像の円、その上に描画されている輪郭のみの円が回帰により得られた値を使って描画した円となっている。
f:id:whg_res:20170215000212p:plain
 完璧ではないにしても、ある程度の予測ができている。コード類はGitHubに掲載する。
https://github.com/whg-res/RegressionSample

1.データ作成

 特に限定はないので、OpenCV等を用いてランダムにパラメタx、y、d、b、g、rに従った円を描画する。せっかくなので、GitHubにコードを掲載する。
https://github.com/whg-res/roundGen
 作成されるデータの形式は以下の通り。

C:\DATA\000000.png	159	112	2	11	82	80
C:\DATA\000001.png	41	212	5	8	204	51
C:\DATA\000002.png	178	14	21	23	64	8
C:\DATA\000003.png	15	137	22	223	85	76

 それぞれ左から、画像のパス、x、y、d、b、g、rとする(画像パスは適宜変更されたい)。

2.prototxtの設定

 データの入力にはMemoryDataLayerを使う。MemoryDataLayer自身は3次元のデータと正解値となるラベル(長さ1のfloat値)の組しか表現できないため、複数の値の回帰には利用できない。そこで下記のサイトを参考にして、入力画像のデータレイヤと正解値のデータレイヤを別で用意することとする。それぞれのラベルは使わずに、ダミーデータとして出力しておく。
http://d.hatena.ne.jp/muupan/20141010/1412895321
 具体的には以下の通り、"data"レイヤの出力は画像情報"data"とラベル情報"dummy1"だが、"dummy1"は参照されない。その代り、"label"レイヤの出力である"label"がchannel数6となっており、この値と出力の誤差を計算することで複数の値の回帰を実現している。

layer {
  name: "data"
  type: "MemoryData"
  top: "data"
  top: "dummy1"
  include {
    phase: TRAIN
  }
  memory_data_param{
    batch_size: 5
    channels: 3
    height: 224
    width: 224
  }
}
layer {
  name: "label"
  type: "MemoryData"
  top: "label"
  top: "dummy2"
  include {
    phase: TRAIN
  }
  memory_data_param{
    batch_size: 5
    channels: 6
    height: 1
    width: 1
  }
}

 同様に、出力層の部分では以下の通り、出力の数を6個に設定し、lossレイヤで推定値と"label"の値の誤差を計算している。識別とは異なり実数値の推定なのでEuclideanLossを使っている。

layer {
  name: "loss3/classifier_"
  type: "InnerProduct"
  bottom: "pool5/7x7_s1"
  top: "loss3/classifier_"
  param {
    lr_mult: 1
    decay_mult: 1
  }
  param {
    lr_mult: 2
    decay_mult: 0
  }
  inner_product_param {
    num_output: 6
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
      value: 0
    }
  }
}
layer {
  name: "loss3/loss3"
  type: "EuclideanLoss"
  bottom: "loss3/classifier_"
  bottom: "label"
  top: "loss3/loss3"
  loss_weight: 1
}

3.ソースコードの解説

 学習、識別のソースコードは基本的には過去の記事と同様の構成になっている。
http://punyo-er-met.hateblo.jp/entry/2017/02/11/211650
 ただし、今回異なるのは、データをすべて0~1のfloat値で与える必要があることである。学習データのラベルをそのまま与えると学習誤差が発散してうまく学習できなかった。データのラベルを256で割り算することで今回のデータでは1以下に収まるので、以下の部分で対応している。

		for (int i = 0; i < TARGET_DIM; i++){
			label[n*TARGET_DIM + i] = stof(entry[i + 1]) / 256.0;
		}

 その他注意点として、今回データレイヤで利用しているダミーデータに対しても実体を与えて処理をかけなければならない。

	const auto train_input_layer = boost::dynamic_pointer_cast<MemoryDataLayer<float>>( net->layer_by_name("data") );
	const auto test_input_layer = boost::dynamic_pointer_cast<MemoryDataLayer<float>>( test_net[0]->layer_by_name("data") );
	float *train_dummy = new float[train_data_size];
	float *test_dummy = new float[test_data_size];
	for (int i = 0; i < train_data_size; i++)	train_dummy[i] = 0;[f:id:whg_res:20170215000212p:plain]
	for (int i = 0; i < test_data_size; i++)	test_dummy[i] = 0;
	train_input_layer->Reset((float*)train_input_data, (float*)train_dummy, train_data_size);
	test_input_layer->Reset((float*)test_input_data, (float*)test_dummy, test_data_size);

 また、テストの結果は1つの画像に対する6つの推定値が連続して存在し、一回のForward()でバッチ数分の推定値が得られていることを考慮してデータを取得する必要がある。

4.実行結果

 1000枚のデータを80epochほど学習させた場合の結果が以下のようなものである。
f:id:whg_res:20170215000212p:plain f:id:whg_res:20170216003257p:plain
左の画像では少しずれているが、だいたいの位置、大きさ、色が再現できているのが分かる。右の画像は高精度に推定で来た例である。定量評価はできていないが、ほとんどの画像は左の画像のように少しずれた結果となった。

※参考
http://d.hatena.ne.jp/muupan/20141010/1412895321