NeuroWhAI의 잡블로그

[TensorFlow] layers.dense에 대한 고찰(?) - tensordot 본문

개발 및 공부/라이브러리&프레임워크

[TensorFlow] layers.dense에 대한 고찰(?) - tensordot

NeuroWhAI 2018. 2. 2. 20:21


이해하기 더럽게 힘드네요.

tf.layers.dense는 densely-connected layer, 즉 아래 사진처럼 흔히 보이는 '완전연결계층'을 만들어줍니다.


텐서플로 책을 보면서 이정도만 알아도 문제가 없었는데
Seq2Seq 예제에서 3차원 input을 dense에 넘기는 코드를 보고 어떻게 이게 가능한지 이해가 안됬습니다.
이전엔 (batch, input size)차원의 입력만 사용해서 내부 가중치 행렬인 kernel은 (input size, output size)차원이겠고
matmul로 행렬곱 연산하는거겠지 했는데 아닌거였습니다.
Seq2Seq 예제에선 Decoder에 128개 유닛의 RNN 셀 계층?을 두고 이 층의 출력인 (batch, step size, 128)차원 텐서를
그대로 dense의 inputs 인수로 사용하고 있었습니다.
3차원 텐서를 Dense층의 입력으로 쓴거죠!
그래서 이것저것 검색하면서 알아낸 정보를 복습 차원에서 정리하고자 이 글을 씁니다.

위 내용에 따르면 dense는 inputs 인자로 넘어온 텐서가 2차원보다 크면
축A는 inputs의 마지막 차원, 축B는 kernel의 첫번째 차원으로해서 tensordot을 수행하고
그 외엔 일반적인 행렬곱(matmul)을 수행한다고 합니다.

잠깐, 축A는 뭐고 축B는 뭐고 tensordot은 또 뭔가요;;
그래서 계속 검색해보았습니다.
tf.tensordotnumpy.tensordot과 같은 역할을 하는 함수입니다.
주요 파라미터로 a, b, axes가 있으며 axes에 지정된 축을 따라 텐서 내적을 수행한다고 합니다.
... 텐서 내적은 또 뭐고 축을 따라서 한다는건 뭔 소리일까요;; 수알못인 저에겐 너무 가혹한 설명입니다.

이 글을 찾았습니다.
이 글 덕분에 적어도 tensordot의 결과 Shape은 알 수 있게 되었습니다.
a의 Shape이 (2, 6, 5)이고 b는 (3, 2, 4)일때 axes=(A, B)=((0), (1))라면
a에서 A차원(0차원)을 제거하고 b에서 B차원(1차원)을 제거한뒤 이어붙힌 Shape이 결과 Shape이 된다고 합니다.
a에서 0번째 차원을 빼면 (6, 5)가 되고 b에서 1번째 차원을 빼면 (3, 4)가 되니까
그냥 이어붙히면 (6, 5, 3, 4)가 되고 이게 tensordot(a, b, ((0,), (1)))의 결과 Shape이 되는겁니다.
때문에 Seq2Seq 예제에서 (batch, step size, 128)을 inputs로 주고 출력 크기를 10으로 했다면
kernel의 Shape은 (128, 10)일테고 tensordot(inputs, kernel, ((2), (0)))을 수행하면
결과의 Shape은 (batch, step size) + (10) = (batch, step size, 10)이 된다는 겁니다.
(axes가 (2, 0)인 이유는 위에서 축A, 축B가 어쩌고 이야기 했을때 나왔습니다)
여기서 또 한가지 알 수 있는건 사라진 차원의 Shape이 둘다 128로 같다는거죠.
뭐 이건 나중으로 미뤄두고...

하지만 정작 inputs와 kernel가 어떻게 연산되는지는 여전히 모르겠습니다.
그러던 중 tensordot 텐서플로 공식 문서에 아래 내용을 봤습니다.


그러니까 Seq2Seq에서 inputs를 aijk로, kernel를 bmn로 해서 보자면
axes가 (2, 0)일때 cijn은 a의 2차원 축 k와 b의 0차원 축 m을 z = 1~(k=m) 범위에서 aijz * bzn를 합산한 값이라는 소리네요.
(써놓고 보니 뭔 X소리지)

(그림 그리기 참 힘드네요.. ㅠ)

inputs은 3차원, kernel은 2차원, output은 3차원 Shape이라는게 딱 보이죠?
axes=(2, 0) 이므로 inputs의 2D축과 kernel의 0D축 요소들이 각각 곱해져서 합해지는 겁니다.
색깔이 그걸 나타내고 있는데 output의 노란색 배경에 작은 초록색 사각형이 있는 칸을 계산한다고 치면
(전면의 좌하단이 원점이라고 가정) cijn에서 i=1, j=2, n=1번째 칸이 되며
k=m=3칸이므로 z는 1~3이 됩니다.
고로 cijn = aij1*b1n + aij2*b2n + aij3*b3n 이렇게 됩니다.
즉, 그림에서 inputs의 노란색 가로줄과 kernel의 초록색 세로줄 요소들끼리 곱해진뒤 총합을 계산하는겁니다.

결과적으로 그냥 inputs[1~batch] 행렬들과 kernel끼리 각각 행렬곱을 batch번 수행하게 되는겁니다.

으어어 너무 힘들다...
그냥 여기서 예시나 계속 붙들고 있어야겠습니다 ㅠㅠ

+ 중첩 for문으로 표현한 예시를 보니까 좀더 이해가 되네요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
= np.random.random((2,3,4))
= np.random.random((5,6,4,3))
 
#tensordot
= np.tensordot(a, b, [[1,2],[3,2]])
 
#loop replicating tensordot
a0, a1, a2 = a.shape
b0, b1, _, _ = b.shape
cloop = np.zeros((a0,b0,b1))
 
#loop over non-summed indices -- these exist
#in the tensor product.
for i in range(a0):
    for j in range(b0):
        for k in range(b1):
            #loop over summed indices -- these don't exist
            #in the tensor product.
            for l in range(a1):
                for m in range(a2):
                    cloop[i,j,k] += a[i,l,m] * b[j,k,m,l]
cs
그러니까 tensordot 연산은
모든 축에 대해서 중첩 for문을 작성해놓고
곱 누적 연산( result[...] += a[...] * b[...] )에서
axes로 지정되지 않은 축은 누적연산(+=) 좌측과 우측에 모두 존재하지만
axes로 지정된 축은 누적연산 우측에서만 존재하므로 결과 축에 포함되지 않게 코드를 작성하는거네요!
이쯤되니 이걸 왜 텐서 축소(Tensor Contraction)라고도 부르는지 알 것 같네요.




Comments