More than Just Lines on a Map: Best Practices for U.S Bike Routes
Transposed Convolution 한국어 튜토리얼
1.
Transposed Convolution
※ 본 글은 A guide to convolution arithmetic for deep learning을 기반으로, 해당 글의 Transposed Convolution
을 좀 더 자세히 설명한 글입니다. 독자가 Convolution에 대해 친숙하다고 가정하고 썼기 때문에 그렇지 않으신 분
들은 해당 글을 읽고 본 글을 읽으시는 것을 권장드립니다.
※ https://github.com/vdumoulin/conv_arithmetic 의 애니메이션을 같이 보시면 더 좋습니다.
흔히 볼 수 있는 Convolution layer는 이미지의 크기를 줄이는데 많이 쓰입니다. 하지만 Convolution을 사용하는
오토인코더의 디코딩 부분이나 이미지의 해상도를 높이는 경우, Convolution과는 반대로 이미지의 크기를 늘릴 수
있는 연산이 필요합니다. Transposed Convolution은 그런 경우에 적합한 연산입니다.
Transposed Convolution 연산의 결과는 같은 커널을 가지는 평범한 Convolution의 backward pass로 정의됩니
다. 크기의 이미지 와 커널 가 있을 때, 이미지 의 크기가 라면,
의 크기는 다시 가 되는 것입니다. 물론 이미지의 크기뿐만 아니
라 이미지 픽셀 하나 하나의 값도 보통 Convolution의 backward pass를 적용한 결과와 같아야 합니다.
수식으로 나타내면 로 정의됩니다.
여기서 알 수 있듯이, Transposed Convolution은 deconvolution이라고 불리는 경우가 있지만 수학적으로 엄밀하
게 따졌을 때 Convolution의 역연산은 아닙니다. 즉 와 의 크기는 같지만 값은 다를 수 있습니다.
실제 코드로 위의 내용을 확인해보겠습니다.
먼저 재현성을 위해 랜덤 시드를 고정해두겠습니다. pytorch 버전은 0.4.1이고 CPU only 입니다.
pytorch의 Convolution 연산은 꼴의 이미지 텐서를 입력으로 받기 때문에 로 설정해주
었습니다. 또 그냥 Convolution의 backward pass와 Transposed Convolution의 forward pass가 같다는 것을 보
여주기 위해 x 의 requires_grad 플래그를 참으로 설정했습니다.
마찬가지로 Transposed Convolution의 backward pass와 Convolution의 forward pass가 같다는 것을 보여주기
위해 z 에서 retain_grad 를 호출했습니다. pytorch에서 non-leaf tensor(원래 있던 텐서들간의 연산으로 만든
텐서)는 따로 설정하지 않으면 gradient를 저장해주지 않습니다.
import torch
import torch.nn.functional as F
torch.manual_seed(61907)
1
2
3
w = torch.randn(1, 1, 3, 3).requires_grad_(True)
x = torch.randn(1, 1, 4, 4).requires_grad_(True)
z = F.conv2d(x, w)
z.retain_grad()
print(z)
------------------------------------------------
tensor([[[[ 0.4441, -1.0809],
[ 3.4175, 0.2582]]]], grad_fn=<ThnnConv2DBackward>)
1
2
3
4
5
6
7
8
2. 이므로 z.backward(y) 를 실행하면 y 에 Convolution(X, W) 의 backward pass
를 적용한 결과가 x.grad 에 저장됩니다.
의 forward pass가 의 backward pass에 를 넣은 것과
같음을 볼 수 있습니다. 비단 뿐만 아니라, 와 같은 크기를 가지는 임의의 텐서에 대해 두 연산 모두 같은 결과를
냅니다(애초에 같은 연산으로 정의되기 때문).
임을 볼 수 있습니다.
의 backward pass가 의 forward pass( )와 같음을 볼
수 있습니다. z.grad.zero_ 를 호출해주는 이유는 위에서 z.backward 를 호출했기 때문에 이미 z.grad 에 쓸
모없는 값이 들어있었기 때문에 초기화 해주는 것입니다.
Convolution으로 Transposed Convolution 나타내기
그런데 잘 생각해보면, Transposed Convolution은 Convolution의 backward pass이고, Convolution의
backward pass 과정에서 서로 영향을 미치는 입력 / 출력 위치의 쌍은 Convolution의 forward pass 과정과 똑같
습니다. 그림으로 나타내면 아래와 같습니다. 일단은 Stride가 1로 고정된 경우만 생각하겠습니다.
z.backward(z)
print(x.grad)
------------------------------------------------
tensor([[[[ 0.0600, -0.0058, -0.7469, 0.9871],
[ 0.7118, 0.9553, -4.1505, -0.1932],
[ 0.9571, 6.4981, -1.3798, 0.4343],
[-7.4335, 3.6245, -1.0891, -0.1062]]]])
1
2
3
4
5
6
7
x_prime = F.conv_transpose2d(z, w)
print(x_prime)
------------------------------------------------
tensor([[[[ 0.0600, -0.0058, -0.7469, 0.9871],
[ 0.7118, 0.9553, -4.1505, -0.1932],
[ 0.9571, 6.4981, -1.3798, 0.4343],
[-7.4335, 3.6245, -1.0891, -0.1062]]]],
grad_fn=<ThnnConvTranspose2DBackward>)
1
2
3
4
5
6
7
8
print(x)
------------------------------------------------
tensor([[[[ 0.1769, -0.6392, -0.4515, 0.3774],
[-1.3197, 2.6374, -1.0508, -0.9508],
[ 1.1264, 0.4620, -0.3078, -1.0129],
[-0.3746, -0.6416, -1.6071, 0.1219]]]], requires_grad=True)
1
2
3
4
5
6
z.grad.zero_()
x_prime.backward(x, retain_graph=True)
print(z.grad)
------------------------------------------------
tensor([[[[ 0.4441, -1.0809],
[ 3.4175, 0.2582]]]])
1
2
3
4
5
6
3. 왼쪽 위를 [0, 0], 왼쪽 아래를 [n, 0], 오른쪽 위를 [0, n], 오른쪽 아래를 [n, n] 좌표로 나타내면
forward pass에서 은 을 통해 과, backward pass에서 은 을 통해 과
연결됨을 볼 수 있습니다.
Stride를 옮길 경우 아래와 같이 연결됨을 볼 수 있습니다.
( 의 회색 칸이 의 회색칸을 거쳐 로 연결됨)
4. ( 의 살구색 칸이 의 살구색칸을 거쳐 로 연결됨)
이 의 각 위치에 대해 어떻게 연결되어 있는지 살펴보면 아래와 같습니다.
그런데 를 상하로 한번, 좌우로 한번 뒤집은 것을 라고 하면, 원래 중간에서 한번 교차하던 화살표들이
예쁘게 정렬되는 것을 볼 수 있습니다.
같은 원리로 의 backward pass, 즉 또한 화살표만 반대 방향인 연결성을 가집니
다.
5. 그런데 Convolution은 기본적으로 커널 와 대응되는 의 패치를 Element-wise Multiplication 한 뒤 모두 더한
것이기 때문에, backward pass 또한 입력 gradient 값에 대응됐던 커널 의 원소를 곱하고, 대응됐던 의 픽셀에
더해주는 것입니다. 즉 위의 그림에서 화살표는 다음 연산을 의미합니다.
forward pass에서 방향 화살표: element-wise multiplication
forward pass에서 방향 화살표: additio
backward pass에서 방향 화살표: element-wise multiplication
backward pass에서 방향 화살표: addition
결국 의 backward pass는 와 같습니다. 다만 가장자리에 있는 픽
셀들까지 고려하면 에 적절한 zero padding을 더해줘야 합니다. zero padding은 커널 크기 - 1 만큼 줘야 합니다.
의 코너부분 픽셀이 를 어떻게 거쳐 와 어떻게 연결되는지를 생각하면 왜 해당 크기만큼 줘야 하는지 알 수
있습니다. 또는 결과의 크기가 원래 와 같아야 함을 생각하셔도 됩니다.
pytorch 코드로 나타내면 아래와 같습니다.
flip 함수는 텐서와 차원이 주어졌을 때, 해당 차원에 대해 텐서를 뒤집습니다.
# https://github.com/pytorch/pytorch/issues/229
import torch
def flip(x, dim):
indices = [slice(None)] * x.dim()
indices[dim] = torch.arange(x.size(dim) - 1, -1, -1,
dtype=torch.long, device=x.device)
return x[tuple(indices)]
1
2
3
4
5
6
7
6. flip_w 는 w 를 상하좌우로 뒤집은 텐서임을 확인할 수 있습니다.
의 backward pass == == 임을 확인할 수 있습
니다.
한가지 추가적으로 확인할 수 있는 점은 Transposed Convolution이 원래 Convolution 처럼 커널을 학습가능한 연
산이라는 것입니다.
행렬곱으로 Convolution 나타내기
그런데 왜 Transposed Convolution이라는 명칭이 붙었을까요? 위에서 살펴본 것처럼 커널을 상하좌우로 뒤집은
뒤 Convolution을 적용하면 같은 결과가 나오니 오히려 Flipped Convolution으로 부르는 것이 맞지 않을까요?
크기의 이미지 , 크기의 커널 이 있을 때 를 구하는 방법을 생각해봅시
다.
로 구할 수 있을 것이고, 나머지 위
치에 대해서도 비슷한 방식으로 구할 수 있습니다.
그런데 원래 2차원 배열인 와 를 1차원 열벡터로 펼치면 적당한 를 정의해서 둘 사이의 관계를 로 나
타낼 수 있습니다. 에 대응되는 열벡터를 라고 썼을 때 아래와 같이 나타낼 수 있고,
flip_w = flip(flip(w, 2), 3)
print(w)
print(flip_w)
------------------------------------------------
tensor([[[[ 0.1352, 0.3158, -0.9132],
[ 0.5627, 1.0118, -0.0394],
[-2.1751, 1.2249, -0.4112]]]], requires_grad=True)
tensor([[[[-0.4112, 1.2249, -2.1751],
[-0.0394, 1.0118, 0.5627],
[-0.9132, 0.3158, 0.1352]]]], grad_fn=<TakeBackward>)
1
2
3
4
5
6
7
8
9
10
print(F.conv2d(z, flip_w, padding=w.shape[2]-1))
print(F.conv_transpose2d(z, w))
------------------------------------------------
tensor([[[[ 0.0600, -0.0058, -0.7469, 0.9871],
[ 0.7118, 0.9553, -4.1505, -0.1932],
[ 0.9571, 6.4981, -1.3798, 0.4343],
[-7.4335, 3.6245, -1.0891, -0.1062]]]],
grad_fn=<ThnnConv2DBackward>)
tensor([[[[ 0.0600, -0.0058, -0.7469, 0.9871],
[ 0.7118, 0.9553, -4.1505, -0.1932],
[ 0.9571, 6.4981, -1.3798, 0.4343],
[-7.4335, 3.6245, -1.0891, -0.1062]]]],
grad_fn=<ThnnConvTranspose2DBackward>)
1
2
3
4
5
6
7
8
9
10
11
12
13
7. 를 다음과 같이 정의하면
가 성립하는 것을 알 수 있습니다.
Convolution의 forward pass를 로 나타낼 수 있으므로, backward pass 또한 를 곱해주는 것,
즉 로 나타낼 수 있습니다. 이 경우 backward pass는 크기가 4인 열벡터를 크기가 16인 열벡터로 바꿔주게 됩
니다.
앞에서도 살펴봤듯이 Transposed Convolution의 정의는 forward pass가 같은 커널을 가지는 Convolution의
backward pass와 같은 연산입니다. 즉 Transposed Convolution도 그냥 Convolution과 마찬가지로 위와 같이 행
렬곱으로 나타낼 수 있고, Transposed Convolution을 행렬곱으로 나타냈을 때의 가중치 행렬이 면, 를 가중
치 행렬로 가지는 Convolution의 동작과 똑같게 됩니다. 이것이 Transposed라는 전치사의 유래입니다.
Stride가 1보다 큰 경우
8. 위에서는 Transposed Convolution을 Convolution으로 나타낼 때 Stride가 1인 경우만 생각했는데, 만약 Stride 값
이 1보다 크면 어떻게 될까요?
의 각 픽셀이 backward pass 과정에서 의 어떤 부분에 대응되는지, 즉 forward pass에서
Receptive Field가 어떻게 되는지 생각하면 쉽습니다. Stride가 1, 커널 크기가 , 이미지 크기가 인
Convolution의 backward pass에 대응되는 Transposed Convolution 연산을 Zero padding을 덧붙이는
Convolution 꼴로 나타낼 경우, Transposed Convolution의 입력 의 각 픽셀에 대응되는 출력 의 범위는 아래
와 같습니다.
의 관점에서 의 Receptive Field는 이고, 의
Receptive Field는 입니다. 상에서 한 칸 움직일 때마다 Receptive Field도 같은
방향으로 한칸 움직이는 것을 볼 수 있습니다.
그런데 원래 Convolution의 입력 이미지 의 크기가 , 커널 의 크기가 , Stride가 2인 경우를 살펴봅시
다.
9.
상에서 한 칸 움직였을 때 Receptive Field는 같은 방향으로 두 칸씩, 즉 Stride 만큼 움직이는 것을 볼 수 있습니
다. 사실 당연한 결과인게, 애초에 를 계산하기 위한 의 patch가 Stride만큼씩 움직이므로 Receptive Field 또한
같이 움직입니다.
Receptive Field가 상에서 어디에 위치하냐만 바뀌고 Receptive Field 상의 픽셀과 의 픽셀 간의 관계( 의 분
홍색 픽셀은 의 분홍색 픽셀을 통해 의 픽셀과 연결돼 있음)는 변하지 않기 때문에, 의 원래 픽셀 사이에
Stride - 1 칸씩 zero padding을 추가해줌으로써 Stride를 반영할 수 있습니다.
10. 를 의 결과로 보는 관점에서, 상의 원래 원소를 기준으로 생각하면 (Transposed
Convolution을 모사하는) 해당 Convolution의 Stride 값은 이 됩니다( 는 를 계산할 때 쓰인
Stride 값). 상에서 칸을 움직여야 그냥 의 다른 원소를 만날 수 있으니 입장에선 그렇게 보이
는 것입니다. 이러한 특성으로 인해 Transposed Convolution은 Fractionally Strided Convolution이라고도 불립니
다.