안녕하세요. 오늘은 torch.nn.ConvTranspose2d 라고 하는 모듈에 대해서 알아보도록 하겠습니다.

 

 

용어로는 Deconvolution이나 fractionally-strided convolution이라고 불리는 작업을 수행합니다.

 

 

해당 작업이 무엇인지를 자세하게 다룬 자료들은 이미 너무 많이 있어 생략하고, 실제로 연산되는 결과를 위주로 살펴보겠습니다.

 

 

이 모듈은 Deep Convolutional GAN을 구현할 때 주로 사용됩니다.

 

 

그럼 시작해보겠습니다.

 

 

 

분석 코드

 

 

import torch
import torch.nn as nn

# sample values.
test_input = torch.Tensor([[[[1, 2, 3], [4, 5, 6]]]])
print("input size: ", test_input.shape)
print("test input: ", test_input)

 

먼저, pytorch를 이용해서 실험해 볼 것이기 때문에 모듈을 불러오고요.

 

input으로 들어갈 tensor를 만들어줍니다.

 

사이즈가 작으면서도 직관으로 이해해보기 쉽도록 다음과 같이 만들었습니다.

 

굳이 4차원으로 만드는 이유는, pytorch에서는 기본적으로 4차원 input만을 지원하기 때문입니다.

 

따라서, 실제로 값이 있는 것은 1부터 6까지로 해서 2 x 3 형태이지만, shape를 맞추기 위해 4차원으로 구성되었다고 보시면 될 것 같습니다.

 

# sample model. It has nn.ConvTranspose2d(1, 3, 4, 1, 0, bias = False)
# First parameter = Channels of input (=1)
# Second parameter = Channels of output (=3)
# Third parameter = Kernel size (=4)
# Fourth parameter = stride (=1)
# fifth parameter = padding (=0)
class sample(nn.Module):
  def __init__(self):
    super(sample, self).__init__()
    self.main = nn.ConvTranspose2d(1, 3, 4, 1, 0, bias = False)

  def forward(self, input):
    return self.main(input)

 

다음으로는 우리가 오늘 살펴보려고 하는 메인인 nn.ConvTranspose2d를 가지고 있는 모델을 하나 만들어줍니다.

 

위에 주석에 달려있듯이, 첫 번째 파라미터는 input의 채널수이고 두 번째 파라미터는 output의 채널 수이며

 

세 번째 파라미터는 kernel size, 네 번째 파라미터는 stride, 다섯 번째 파라미터는 padding입니다.

 

계산을 조금 더 간단하게 하기 위해 bias= False로 놓고 실험해보겠습니다.

 

별도의 신경망 연산이 필요 없으므로, 매우 간단하게 layer 딱 1개만 있는 모델을 만들었습니다.

 

 

 

Model = sample()

# Print model's original parameters.
for name, param in Model.state_dict().items():
  print("name: ", name)
  print("Param: ", param)
  print("Param shape: ", param.shape)

 

만들어진 모델의 기본 weight를 살펴보겠습니다.

 

pytorch가 기본적으로 지원하는 초기화 방법에 따라, weight 값이 결정된 것을 확인할 수 있습니다.

 

그리고 shape는 [1, 3, 4, 4]를 가지게 됩니다. 

 

이는 kernel size를 4로 지정했기 때문에, filter의 사이즈가 4x4 형태가 되어서 그렇고 output의 channel을 3으로 지정하였으므로 결괏값이 3개의 채널로 나와야 해서 3이 생겼다고 보시면 되겠습니다.

 

쉽게 해당 convolution 필터를 가로가 4, 세로가 3, 높이가 4인 직육면체로 생각하시면 편합니다.

 

그림으로 표현해보자면 다음과 같은 것이죠.

 

다음으로는 결과를 조금 더 쉽게 보기 위해서, model의 파라미터를 manually 하게 바꿔보겠습니다.

 

지금은 weight가 소수점으로 되어 있고 매우 복잡하기 때문에, 결과를 직관적으로 이해해볼 수 있기 위한 작업입니다.

 

# I makes 48 values from 0.1 to 4.8 and make (1, 3, 4, 4) shape
np_sam = np.linspace(0.1, 4.8, num = 48)
np_sam_torch = torch.Tensor(np_sam)
sam_tor = np_sam_torch.view(1, 3, 4, 4)


# Modify model's parameters using 4 for loops.
with torch.no_grad():
    batch, channel, width, height = Model.main.weight.shape
    
    for b in range(batch):
        for c in range(channel):
            for w in range(width):
                for h in range(height):
                    Model.main.weight[b][c][w][h] = sam_tor[b][c][w][h]

# Check parameter modification.
print("Model weight: ", Model.main.weight)

 

먼저, np_sam이라는 변수로 np.linspace를 이용해 0.1부터 4.8까지 48개의 값을 만들어줍니다. 이렇게 하면 0.1, 0.2, 0.3 .... 4.8까지 0.1을 간격으로 하는 숫자 48개를 만들 수 있습니다.

 

그리고 이를 torch.Tensor로 바꿔주고, shape를 맞추기 위해 (1, 3, 4, 4)의 형태로 맞춰줍니다.

 

다음으로는 model의 파라미터를 수동으로 바꿔주는 코드입니다.

 

먼저 batch, channel, width, height라는 변수로 model의 weight의 shape를 받습니다.

 

그리고 이를 4중 for문을 이용해서 바꿔줍니다.

 

Model.main.weight[b][c][w][h]의 값을 해당하는 인덱스와 동일한 sam_tor의 위치의 값으로 바꾸는 것이죠.

 

물론 일반적으로 computational cost 때문에 4중 for문을 사용하는 것은 비효율적이긴 하지만, 어쨌든 이번 상황에서는 각 차원별로 값이 작기 때문에 연산 자체가 오래 걸리진 않아 이렇게 코드를 짜보았습니다.

 

그리고 마지막으로는 model의 weight들을 print 해봅니다. 실제로 우리가 원하는 대로 잘 바뀌었는지 봐야겠죠.

 

역시나 원하는 대로 잘 바뀌었네요. 

 

이렇게 값들이 simple해야 우리가 결과를 찍었을 때 해석도 쉬울 것입니다.

 

이것도 아까와 마찬가지로 shape가 (1, 3, 4, 4)가 될 텐데요, 이 또한 직육면체 형태로 이해해 볼 수 있습니다.

 

모든 원소의 값이 다 보이시도록 최대한 길게 그렸는데요. 이처럼 가로가 4, 세로가 3, 높이가 4를 만족하는 직육면체를 생각하시면 됩니다.

 

 

result = Model(test_input)

print("Result shape: ", result.shape)
print("Result: ", result)

다음으로는, 아까 만들어뒀던 (1, 1, 2, 3) 짜리 input을 위에서 보여드린 weight을 이용해서 nn.ConvTranspose2d를 사용해 연산한 결과를 출력합니다.

 

결과가 어떻게 나오는지 보겠습니다.

 

 

뭔가 어떤 결과가 나왔는데, 이것만 봐서는 아직 어떤 식으로 연산이 이루어지는지 알 수 없겠죠?

 

그래서 제가 하나하나 한번 뜯어서 분석해보았습니다.

 

 

 

실제 구동 방법 분석

 

 

이해를 조금 쉽게 하기 위해서, 파라미터와 결과 모두 channel을 1차원으로 가정하고 살펴보겠습니다.

 

먼저, 우리의 input은 다음과 같은 2 x 3 형태의 tensor가 됩니다.

 

이를 우리가 설정한 parameter를 이용해서 단순히 곱해줍니다.

 

따라서 다음과 같은 결과를 볼 수 있습니다.

 

 

다음으로는 다른 input 요소들도 어떻게 계산되는지 보겠습니다.

 

 

 

 

 

 

 

자 그러면 input 1부터 6까지 연산을 완료했다면, 결과가 총 6개가 나오게 되겠죠?

 

나온 6개의 결과를 element-wise addition을 통해서 최종 결과를 뽑아냅니다.

 

 

이것이 아까 프로그램을 돌려서 나온 결과와 일치함을 확인할 수 있습니다.

 

 

 

 

따라서, nn.ConvTranspose2d의 연산 과정을 다음과 같이 정리해볼 수 있습니다.

 

 

1. nn.ConvTranspose2d를 만들 때 input값의 channel 수와 설정한 output channel 수, kernel size를 고려하여 weight를 만들어낸다.

 

우리의 경우에는 output channel을 3으로 지정하였고, kernel size를 4로 지정하였으므로 

 

weight의 크기가 (1, 3, 4, 4)가 됩니다. 

 

만약에 input의 channel 수가 3이고, output channel 수가 7이며 kernel size가 4였다면

 

weight의 크기가 (3, 7, 4, 4)가 될 것입니다.

 

즉, weight의 크기는 (input channel, output channel, kernel size, kernel size)가 됩니다.

 

 

 

2. input의 각 element 별로 weight와 곱해줍니다. 만약 stride가 1보다 크다면, 그 값만큼 이동하면서 만들어줍니다.

 

위에서의 예시는 stride 1인 경우를 봤었죠. input의 각 element 별로 weight와 곱해준 결과를 stride 1로 이동하면서 만들어냈습니다.

 

 

 

 

3. 나온 모든 결괏값을 element-wise 하게 더해서 최종 결과를 냅니다.

 

stride가 kernel size보다 작게 되면 서로 겹치는 부분들이 존재합니다. 그럴 때는 단순히 element-wise하게 더해서 결과를 내주면 됩니다.

 

 

 

마지막으로, batch size가 1보다 클 때의 결과도 한번 정리해보면서 마무리 짓겠습니다.

 

만약 input이 (4, 3, 4, 4)의 사이즈를 가지고 있고, 모델이 nn.ConvTranspose2d(3, 7, 4, 1, 0, biase = False)인 layer를 가지고 있다고 가정한다면 어떻게 될까요?

 

먼저 parameter의 값들부터 확인해보겠습니다.

 

input channel이 3이고, output channel이 7이며, kernel size는 4이고, stride는 1이며 padding은 0인 경우입니다.

 

 

그렇다면 weight는 shape가 어떻게 될까요?

 

 

위에서 얘기한 내용을 적용해보면, (3, 7, 4, 4)가 됨을 알 수 있습니다.

 

 

마지막으로 output의 결과는 어떻게 될까요?

 

 

batch size는 그대로 4가 될 것이고, output channel을 7로 정했으니 channel 수는 7이 되겠죠?

 

 

그리고 stride = 1인 상황이고 padding = 0이니까, input 각각에 대해서 kernel size 4 만큼 확장이 되므로

 

 

이것이 1칸씩 이동하는데 input 사이즈가 4개니까, 총 7이 됩니다. (input이 1개면 4, 2개면 5, 3개면 6, 4개면 7이 되는 것이죠. stride = 1이므로 하나씩 이동한다고 보시면 될 것 같아요.)

 

따라서 output의 결과는 (4, 7, 7, 7)이 됩니다.

 

 

 

지금까지 nn.ConvTranspose2d가 어떻게 작동하는지에 대해서 세세하게 알아봤습니다.

 

예시에서는 조금 더 와닿게 만들기 위해서 batch size가 1일 때를 가정하고 설명하였으나,

 

사실상 batch size는 input와 output에서 변동되는 요소가 아니므로 크게 신경 쓰지 않아도 되는 부분이라고 생각합니다.

 

 

혹시라도 보시면서 더 궁금하신 점이나 이해가 안 되시는 점이 있다면 댓글로 추가적으로 설명을 드릴 수 있을 것 같습니다.

 

 

이번 포스팅은 여기서 마치겠습니다.

 

 

 

 

+ Recent posts