シリーズで、NNablaのPython-like C++APIをご紹介したいと思います。
第四回目は、ResNetのサンプルを元にして、ConvolutionやBatchnormの進んだ使い方をご紹介いたします。
分類モデル用ニューラルネットワークの代表例であるResNet(Residual Network:残差ネット)を説明します。
ResNetは、マイクロソフト・リサーチのKaiming Heらから提唱されたネットワーク構造です。
一般物体認識の性能コンペティションとして知られるイメージネット・チャレンジで、2015年度優勝したとときの手法として知られています。
以下はResNetの論文です。
Deep Residual Learning for Image Recognition
Kaiming He, Xiangyu Zhang, Shaoqing Ren, Jian Sun
https://arxiv.org/abs/1512.03385
ResNetは、ResUnitと呼ばれる残差を算出するネットワークを基本単位として、これを深さ方向に複数直結させた構造をしています。
ResUnitは、入力を、2つのパスに分けて、出力時に再度加算する構成をしています。
2つのパスの片方は入出力が直結した構成をしています。
もう片方は、入出力の間に畳み込み層など学習可能な層を挟まれた構成をしています。
ResUnitを複数直結させることで、ネットワークの深さ(入力から出力までの総数)は深くできます。
また、入出力が直結するパスが支配的になるように学習することで、畳み込み層をバイパスする効果が得られ、深くなりすぎることを回避できるとされています。
ResNetの実行ファイルを作るプログラムは、以下の3つのソースコードファイルで構成されています。
以下では、プログラムのコアにあたるresnet_training.hppから、主として、LeNetでは、説明しきれなかった内容を説明します。
ResNet学習プログラム:resnet_training.hpp
このソースコードファイルでは、ResNetによる分類モデルの記述と、モデルのビルド、学習、評価の記述を行っています。
モデルのビルド、学習、評価の記述は、LeNetでの説明と大きな違いはありません。
ここでは、ResNetのモデルの記述について、LeNetのモデルでは説明しきれなかった内容を中心に紹介します。
- Augmentationの記述
ResNetは、学習時に学習データの水増し(Augmentation)をすることで、分類精度を向上しています。
NNablaのPythonAPIでは、image_augmentationと名付けられた画像データの水増しを行う層を用意しています。
この層は、Python-like C++APIでも利用できます。
以下は、ブール値augmentationのフラグに応じて、学習データの水増し層を利用する関数例です。
引数はPythonAPIと同様であり、NNablaのドキュメンテーションが参考になります。
CgVariablePtr augmentation(CgVariablePtr h, bool augmentation) {
if (augmentation) {
return f::image_augmentation(h, {1, 28, 28}, {0, 0}, 0.9, 1.1, 0.3, 1.3, 0.1, false, false, 0.5, false, 1.5, 0.5, false, 0.1, 0);
} else {
return h;
}
}
- ResUnitの記述
ResUnitは、入力から直結したパスの出力と、学習可能なパスからの出力を、加算して出力する構成です。
学習可能なパスは、3つのコンボリューション層、バッチノーマライゼーション層、活性層(以下ではElu)で構成されています。
以下は、ResUnitの関数例です。
CgVariablePtr res_unit(CgVariablePtr x, bool test, ParameterDirectory parameters) {
Shape_t shape_x = x->variable()->shape();
const int n_channel = shape_x[1];
pf::ConvolutionOpts opts1 = pf::ConvolutionOpts().with_bias(false);
pf::ConvolutionOpts opts2 = pf::ConvolutionOpts().with_bias(false).pad({1, 1});
pf::ConvolutionOpts opts3 = pf::ConvolutionOpts().with_bias(false);
auto c1 = pf::convolution(x, 1, n_channel / 2, {1, 1}, parameters["conv1"], opts1);
auto e1 = f::elu(pf::batch_normalization(c1, !test, parameters["conv1"]), 1.0);
auto c2 = pf::convolution(e1, 1, n_channel / 2, {3, 3}, parameters["conv2"], opts2);
auto e2 = f::elu(pf::batch_normalization(c2, !test, parameters["conv2"]), 1.0);
auto c3 = pf::convolution(e2, 1, n_channel, {1, 1}, parameters["conv3"], opts3);
auto e3 = f::elu(pf::batch_normalization(c3, !test, parameters["conv3"]), 1.0);
return f::elu(f::add2(e3, x, true), 1.0);
}
コンボリューションの引数
プログラム例にあるConvolutionOptsクラスは、コンボリューションの引数のうち、変更したい引数だけを記述するためのクラスです。
この例は、with_bias引数と、pad引数を変更する例で、メンバ参照演算子(.)を用いて変更します。
同時に複数の引数を変更したい場合、メンバ参照演算子を複数接続して変更します。
このようにしてカスタマイズを記述したConvolutionOptsクラスを、コンボリューションの引数として入力することで、所望のカスタマイズが得られます。
コンボリューションの引数は、たとえば、LeNetなどの基本的な構成では、フィルタサイズ(カーネルサイズ)、出力チャンネル数が指定されれば十分です。
しかし、コンボリューションをよりカスタマイズしたい場合、フィルタサイズ、出力チャンネル数以外にも、パッドサイズ(入力の周辺に追加するピクセル数)、ストライド幅(入力をスキャンするときの移動幅)、グループ数(畳み込み後のチャンネル結合の結合単位数)、ダイレーション(フィルタの間引き間隔)などの項目があります。
これらの項目のデフォルト値は、NNablaのPythonAPIのドキュメントが参考になります。
ResUnitの場合、コンボリューション層を、バイアス項の有無、パッド(画像周辺のピクセルを仮想的に追加)のサイズをカスタマイズして用いることが通例です。
上述のプログラム例もそのような通例を反映しています。
ConvolutionOpts以外の引数変更クラスには、AffineOpts、BatchNormalizationOpts、PoolingOptsがあります。
これらについては、実際に使われているサンプルプログラムで説明することにします。
バッチノーマライゼーションの引数
プログラム例にあるbatch_normalization関数の第二引数は、batch_stat引数と呼ばれ、ミニバッチ内での変数の正規化(標準化)に際して、どのような統計量を用いるかを指示する引数です。
通常、学習時は、batch_statをtrueに設定し、バッチ統計量と呼ばれる、ミニバッチ内での平均、分散の統計量を変数の標準化に用います。
また、テスト評価時はbatch_statをfalseに設定し、ランニング統計量と呼ばれる学習時のバッチ統計量を学習の繰り返し処理で平均化した値を用います。
プログラム例の関数の引数testは、この関数をテスト用ネットワークとして用いる場合にtrueとする引数です。
バッチノーマライゼーションの第二引数であるbatch_statに、!testの値を入力することで学習用ネットワークとテスト用ネットワークを切り替えます。
- ResNetの記述
ResNetは、ResUnitを複数、深さ方向に直結したネットワーク構造をしています。
以下のプログラム例は、最初にRGBチャンネル入力に対するコンボリューション・バッチ正規化を施したのち、ResUnitを5ユニット分繰り返して、得られた特徴から全結合層でクラスの確率を算出するためのロジットを算出する構成です。
CgVariablePtr model(CgVariablePtr x, bool test, ParameterDirectory parameters) {
auto xa = x * (1.0 / 255.0);
xa = augmentation(x, !test);
pf::ConvolutionOpts opts = pf::ConvolutionOpts().with_bias(false).pad({3, 3});
auto c1 = pf::convolution(x, 1, 64, {3, 3}, parameters["conv1"], opts);
c1 = f::elu(pf::batch_normalization(c1, !test, parameters["conv1"]), 1.0);
auto c2 = f::max_pooling(res_unit(c1, test, parameters["conv2"]), {2, 2}, {2, 2});
auto c3 = f::max_pooling(res_unit(c2, test, parameters["conv3"]), {2, 2}, {2, 2});
auto c4 = res_unit(c3, test, parameters["conv4"]);
auto c5 = f::max_pooling(res_unit(c4, test, parameters["conv5"]), {2, 2}, {2, 2});
auto c6 = res_unit(c5, test, parameters["conv6"]);
auto h = f::average_pooling(c6, {4, 4}, {4, 4});
auto y = pf::affine(h, 1, 10, parameters["classifier"]);
return y;
}