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ほどやってもあまり目に見えて変わらなかったので、もっと複雑なモデルで比較する必要があるかもしれません。

おすすめ

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です