LoginSignup
3
2

More than 5 years have passed since last update.

密行列積と全結合層のforward/backwardに関するメモ

Last updated at Posted at 2018-04-25

数値線形代数はがっつりやったことあるけど、ディープラーニングとか分からん勢のみなさん(=私)、こんばんは。

ディープラーニングをしていると全結合層(ChainerでいうとLinear)がよく現れて、これが密行列積と同じだと言われるんですが、じゃあ実際にどういう対応になってるのか?をいつも忘れるので、メモがてらまとめておきます。

密行列積の基本

ここでは行優先とします。

C=AB

ここで

  • C:n行m列
  • A:n行l列
  • B:l行m列

です。見慣れたアインシュタイン縮約表記だと

C_{ij} = A_{ik} B_{kj}

になるので、この計算は

for(i = 0; i < n; ++i)
{
    for(j = 0; j < m; ++j)
    {
        for(k = 0; k < l; ++k)
        {
            C[i][j] += A[i][k] * B[k][j]
        }
    }
}

で計算できることも当然のことですね。

入出力

入力(画像)をX、出力(画像)をYとします。

畳み込みニューラルネットワーク(CNN)のような場合、XとYは「チャンネル数」行「バッチサイズ」列の行列になります。
例えば、ResNet152では最終段に全結合層がありますが、これは「入力チャンネル数=2048」「出力チャンネル数=1024」です。バッチサイズは任意のサイズですが、ここでは適当に128とします。

そうすると、

  • X:2048行128列
  • Y:1024行128列

ということになります。

Forward(推論)

XからYを生成する(推論)時は、XとWの積で計算します。つまり、Wは「入力チャンネル数」行「出力チャンネル数」列の行列になります。これは

Y=WX

という密行列積そのものです。ここで各行列のサイズは、先の例を入れると

  • Y:1024行128列
  • W: 1024行2048列
  • X:2048行128列

になります。

Backward(学習)

学習時のBackward計算では、X, Y, Wのそれぞれの勾配gX, gY, gWが出てきます。gX, gY, gWのサイズはそれぞれX, Y, Wのサイズと一致します。

ここでは、"backward"といっているのですから、XとWからYを計算した時の逆、出力(gY)から入力(gX)と重み(gW)を計算します。
計算式は、結論だけ書くと、以下のようになります(なぜこうなるかは世界に一杯あるディープラーニングの教科書を参照してください)。

g_X = W^T g_Y
g_W = g_Y X^T

というわけで、転置が入っていますが、これも密行列積そのものです。先の例を代入すると

  • 2048行1024列と1024行128列の積=2048行128列
  • 1024行128列と128行2048列の積=1024行2048列

という計算になります。

Chainerと比較してみる

おまけとして、Chainerと比較してみます。

INPUT_CHANNEL = 2048
OUTPUT_CHANNEL = 1024
BATCH_SIZE = 128

import numpy
def calc_numpy():
    DTYPE=numpy.float32

    W = numpy.random.rand(OUTPUT_CHANNEL, INPUT_CHANNEL).astype(DTYPE)
    X = numpy.random.rand(INPUT_CHANNEL, BATCH_SIZE).astype(DTYPE)

    # forward
    Y = W.dot(X)

    gY = numpy.random.rand(Y.shape[0], Y.shape[1]).astype(DTYPE)

    # barkward
    gX = W.transpose().dot(gY)
    gW = gY.dot(X.transpose())

    return Y, gX, gW, X, W, gY


import chainer
def calc_chainer(X, W, gY):
    linear = chainer.links.Linear(W.shape[1], W.shape[0], initialW=W)
    linear.cleargrads()

    X_var = chainer.Variable(numpy.array(X).transpose())

    # forward
    Y_var = linear(X_var)

    # barkward
    Y_var.grad = gY.transpose()
    Y_var.backward()

    return Y_var.data.transpose(), X_var.grad.transpose(), linear.W.grad

def main():
    Y_numpy, gX_numpy, gW_numpy, X, W, gY = calc_numpy()
    Y_chainer, gX_chainer, gW_chainer = calc_chainer(X, W, gY)

    print("Is Y same?",  numpy.array_equal(Y_numpy, Y_chainer))
    print("Is gX same?", numpy.array_equal(gX_numpy, gX_chainer))
    print("Is gW same?", numpy.array_equal(gW_numpy, gW_chainer))

if __name__ == "__main__":
    main()

最初Chainer側の入出力は列優先なのかと思ったがそうでもなくてWだけ行優先なので、ちょっと変な感じです。まぁそもそも、ディープラーニングが数値線形代数とは違う分野なので仕方ないですね。

・・・もしかして、「ディープラーニングとかエーアイをやるためには線形代数が必要だよ!」と言われているのに初心者の人がとっつきにくい原因はこの転置のせいなのでは説

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2