数値線形代数はがっつりやったことあるけど、ディープラーニングとか分からん勢のみなさん(=私)、こんばんは。
ディープラーニングをしていると全結合層(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だけ行優先なので、ちょっと変な感じです。まぁそもそも、ディープラーニングが数値線形代数とは違う分野なので仕方ないですね。
・・・もしかして、「ディープラーニングとかエーアイをやるためには線形代数が必要だよ!」と言われているのに初心者の人がとっつきにくい原因はこの転置のせいなのでは説