ResNetで使われるShortcut layerの実装[c++ Arrayfire]
六花です。
層の抽象化によってモデルの構造をとても深くすることが簡単になったので、ResNetに必要なShortcut Layerの実装をしてみます。
Shortcut Layerの詳しい説明は、以下のページが参考になりました。
Residual Network(ResNet)の理解とチューニングのベストプラクティス
https://deepage.net/deep_learning/2016/11/30/resnet.html
§環境
windows 10
Microsoft Visual Studio Community 2022 (64 ビット) Version 17.3.4
ArrayFire v3.8.2
struct Shortcut_layer : public Layer
{
const af::array &x1, &x2;
af::array &y;
af::array &dx1, &dx2;
const af::array &dy;
Shortcut_layer(const af::array& x1, const af::array& x2, af::array& y, af::array& dx1, af::array& dx2, const af::array& dy)
: x1(x1)
, x2(x2)
, y(y)
, dx1(dx1)
, dx2(dx2)
, dy(dy)
{
dx1 = 0.0;
}
virtual void forward()
{
y = x1 + x2;
y.eval();
}
virtual void backward()
{
dy.eval();
dx1 += dy;
dx2 += dy;
}
};
Shortcut layerは三つの層と関連がある処理層なので、コンストラクタの引数の数が他の処理層と異なります。
実装の際、メンバ変数の宣言で参照が外れていないか確認する必要があります。
const af::array& x1, x2; // bad
上記のように書くと、x2は参照ではないので計算が正しく行われません。
// モデル構築
std::vector<af::array> data; // 評価値
data.push_back(af::constant(0.0, size_input, size_batch, dtype_t));
for (int i_hidden_layer = 0; i_hidden_layer < size_hidden_layer; ++i_hidden_layer)
{
for (int i = 0; i < 5; ++i)
{
data.push_back(af::constant(0.0, size_hidden, size_batch, dtype_t));
}
}
data.push_back(af::constant(0.0, size_hidden, size_batch, dtype_t));
data.push_back(af::constant(0.0, size_hidden, size_batch, dtype_t));
data.push_back(af::constant(0.0, size_output, size_batch, dtype_t));
std::vector<af::array> grad; // 誤差(傾き)
grad.push_back(af::constant(0.0, size_input, size_batch, dtype_t));
for (int i_hidden_layer = 0; i_hidden_layer < size_hidden_layer; ++i_hidden_layer)
{
for (int i = 0; i < 5; ++i)
{
grad.push_back(af::constant(0.0, size_hidden, size_batch, dtype_t));
}
}
grad.push_back(af::constant(0.0, size_hidden, size_batch, dtype_t));
grad.push_back(af::constant(0.0, size_hidden, size_batch, dtype_t));
grad.push_back(af::constant(0.0, size_output, size_batch, dtype_t));
std::vector<std::shared_ptr<Layer>> layer; // 処理層
layer.push_back(std::make_shared<FC_layer>(data.at(0), data.at(1), grad.at(0), grad.at(1)));
for (int i_hidden_layer = 0; i_hidden_layer < size_hidden_layer * 5; i_hidden_layer += 5)
{
layer.push_back(std::make_shared<tanhExp_layer>(data.at(i_hidden_layer + 1), data.at(i_hidden_layer + 2), grad.at(i_hidden_layer + 1), grad.at(i_hidden_layer + 2)));
layer.push_back(std::make_shared<FC_layer/* */>(data.at(i_hidden_layer + 2), data.at(i_hidden_layer + 3), grad.at(i_hidden_layer + 2), grad.at(i_hidden_layer + 3)));
layer.push_back(std::make_shared<tanhExp_layer>(data.at(i_hidden_layer + 3), data.at(i_hidden_layer + 4), grad.at(i_hidden_layer + 3), grad.at(i_hidden_layer + 4)));
layer.push_back(std::make_shared<FC_layer/* */>(data.at(i_hidden_layer + 4), data.at(i_hidden_layer + 5), grad.at(i_hidden_layer + 4), grad.at(i_hidden_layer + 5)));
layer.push_back(std::make_shared<Shortcut_layer>(
data.at(i_hidden_layer + 1), data.at(i_hidden_layer + 5), data.at(i_hidden_layer + 6),
grad.at(i_hidden_layer + 1), grad.at(i_hidden_layer + 5), grad.at(i_hidden_layer + 6)
));
}
layer.push_back(std::make_shared<tanhExp_layer>(data.at(size_hidden_layer * 5 + 1), data.at(size_hidden_layer * 5 + 2), grad.at(size_hidden_layer * 5 + 1), grad.at(size_hidden_layer * 5 + 2)));
layer.push_back(std::make_shared<FC_layer/* */>(data.at(size_hidden_layer * 5 + 2), data.at(size_hidden_layer * 5 + 3), grad.at(size_hidden_layer * 5 + 2), grad.at(size_hidden_layer * 5 + 3)));
モデル構築は段々とわかりづらくなってきました。
ここまでくると人によって書き方はだいぶ変わると思います。
Shortcut layerは二つの層の値を加算する処理層なので、値が確定する後ろの方に設置します。
単純に一個ずつ積んでいけば良い層ではないので、gradient checkingで確認しながらのモデル構築が求められます。
実際にsize_hidden_layerが1のときと2のときで収束具合を見てみます。
// size_hidden_layer = 1 epoch : 1 diff : 8.07484 norm : 112.098 epoch : 101 diff : 7.15597 norm : 103.267 epoch : 201 diff : 5.16253 norm : 73.4967 epoch : 301 diff : 4.64482 norm : 61.9452 epoch : 401 diff : 2.65903 norm : 36.1601 epoch : 501 diff : 1.32206 norm : 19.3017 epoch : 601 diff : 0.847383 norm : 12.8314 epoch : 701 diff : 0.727995 norm : 10.6776 epoch : 801 diff : 1.13575 norm : 16.1485 epoch : 901 diff : 0.66345 norm : 10.1143 epoch : 1001 diff : 0.997691 norm : 13.5542 epoch : 1101 diff : 0.508484 norm : 7.72716 epoch : 1201 diff : 1.40017 norm : 18.235 epoch : 1301 diff : 1.04162 norm : 13.5992 epoch : 1401 diff : 0.544048 norm : 7.9774 epoch : 1501 diff : 0.512846 norm : 7.1423 epoch : 1601 diff : 0.498923 norm : 7.62987 epoch : 1701 diff : 0.461126 norm : 6.58718 epoch : 1801 diff : 0.604371 norm : 8.40605 // size_hidden_layer = 2 epoch : 1 diff : 8.19504 norm : 113.125 epoch : 101 diff : 4.21247 norm : 61.7355 epoch : 201 diff : 1.11258 norm : 14.7781 epoch : 301 diff : 0.417049 norm : 6.67394 epoch : 401 diff : 0.326344 norm : 5.68149 epoch : 501 diff : 0.323183 norm : 4.6097 epoch : 601 diff : 0.44656 norm : 6.10839 epoch : 701 diff : 0.194395 norm : 2.75591 epoch : 801 diff : 0.145993 norm : 2.16209 epoch : 901 diff : 0.200061 norm : 2.78567 epoch : 1001 diff : 0.211442 norm : 3.16593 epoch : 1101 diff : 0.161485 norm : 2.37339 epoch : 1201 diff : 0.170221 norm : 2.49505 epoch : 1301 diff : 0.173115 norm : 2.51095 epoch : 1401 diff : 0.128161 norm : 1.9773 epoch : 1501 diff : 0.128134 norm : 2.01493 epoch : 1601 diff : 0.134621 norm : 1.98662 epoch : 1701 diff : 0.247699 norm : 3.33865 epoch : 1801 diff : 0.13076 norm : 2.01532
モデルの複雑さの割には、あまりdiffやnormが下がりませんでした。
この後ためしに50000epochほどやってもあまり目に見えて変わらなかったので、もっと複雑なモデルで比較する必要があるかもしれません。