[액션파워 LAB] pytorch Conv2d parameter 파헤치기
AI 개발자분들은 매일 방대한 크기의 라이브러리를 살펴보고, 또 그 속에서 수많은 함수들을 마주합니다. 그러다 보면 이 함수의 각 파라미터가 어떤 역할을 하는지 일일이 알기 어려울 때도 있고, 힘들게 직접 구현해 놓은 기능이 이미 지원되던 기능인 것을 알고 허탈할 때도 많습니다. 😢
AI 분야에 막 입문하시는 분들은 더욱 이런 경험이 많으실 텐데요. 오늘은 이런 분들을 위해서 AI 분야에서 가장 대표적인 신경망인 2차원 합성곱 층(2D convolutional layer)의 구현체 pytorch 라이브러리의 torch.nn.Conv2d 클래스를 아주 자세하게 파헤쳐 보겠습니다. 😎
*이 글은 pytorch 1.12 documentation, source code를 바탕으로 작성되었습니다.
What is 2D Convolutional Layer?
본격적으로 살펴보기에 앞서, 2차원 합성곱 층의 개념부터 짚고 넘어가겠습니다. 2차원 합성곱 층은 합성곱 신경망(CNN)의 구성요소이자 ResNet, Res2Net 등의 다양한 아키텍처를 이해하기 위한 선행 개념입니다.
추상적인 연산 방식은 “네모난 모양의 필터가 이미지를 전체적으로 훑으면서 특징을 뽑아내는 것”인데요, 그림과 함께 보다 자세하게 살펴보겠습니다.
이미지라고 언급했던 input 데이터는 실제 이미지일 수도, 이전 층의 output 일 수도 있습니다. pytorch에서는 층간에 주고받는 데이터를 보통 텐서(tensor)라고 부르기에 용어를 input 텐서로 통일하도록 하겠습니다.
네모난 모양의 필터는 pytorch에서 보통 kernel이라 합니다. kernel이 input 텐서를 왼쪽 위부터 오른쪽 아래까지 일정한 간격으로 움직이는데요, 이렇게 움직일 때마다 하나의 값(특징)을 계산하게 됩니다. 하나의 input 텐서로부터 kernel이 계산한 특징들을 모아놓은 것이 바로 feature map입니다.
즉, 2차원 합성곱 층을 추상적으로 설명했던 앞선 문장을 정확하게 다시 기술하자면, “직육면체 형태의 kernel이 input 텐서 위를 일정한 간격으로 움직이며 feature map을 계산하는 것”입니다.
기초 파라미터 파헤치기
이제 각 파라미터가 어떤 의미를 가지는지, 무엇과 연관되는지 기본적인 인자부터 하나하나 살펴보겠습니다.
in_channels
type: int
input 텐서의 차원과 관련된 변수입니다. 2차원 합성곱 층이기 때문에 input 텐서의 차원은 몇 가지 종류로 제한됩니다. 여러 3차원 텐서가 묶여 있는 batch 단위라면 (N, C, H, W), 한 개짜리라면 (1, C, H, W) 혹은 (C, H, W)일 것이죠. 여기서의 C를 보통 채널 차원이라고 하고, 이를 in_channels로 설정해 줘야 합니다.
* 이때, N은 한 batch에 있는 3차원 텐서의 개수, H는 각 데이터의 높이, W는 각 데이터의 너비입니다.
입문자분들은 차원만 보고 그 텐서가 어떤 형태인지 감이 잘 안 오실 텐데요, 아래 그림과 같이 3차원 텐서는 직육면체를 떠올리시면 되고, 4차원의 경우는 그 직육면체가 여러 개 있다고 생각하시면 됩니다.
out_channels
type: int
in_channels는 input 텐서의 차원에 의존하지만, out_channels은 그렇지 않습니다. 이름에서 유추할 수 있듯이 설정한 값이 output 텐서의 채널 차원이 됩니다. 하지만 이는 결과론적인 이야기인데요, 보다 본질적인 의미는 합성곱 층의 kernel 개수입니다.
하나의 kernel은 input 텐서를 전체적으로 지나가면서 하나의 feature map을 만듭니다. kernel이 2개라면 feature map도 2개가 생길 것입니다. 따라서 이 인자로 설정한 값(개수) 만큼의 feature map이 만들어지게 됩니다. 그렇기에 out_channels는 output 텐서의 채널 차원이 됩니다.
kernel_size
type: int | Tuple[int, int]
인자의 이름 그대로 kernel 단면의 크기입니다. int 값을 준다면 정사각형 단면의 kernel로 동작하고, tuple 값을 준다면 그 가로 세로에 해당하는 직사각형 단면의 kernel로 동작합니다.
계속해서 단면의 크기임을 강조하는 이유는 많은 분들이 kernel은 직사각형 모양이라고 알고 계시기 때문입니다. 그러나 대부분의 상황에서 kernel은 3차원 텐서로, 직육면체 형태입니다. 합성곱 층의 연산 방법상, [그림 1]과 같이 kernel의 깊이(높이)는 input 텐서의 채널 차원과 같아야 하기 때문입니다.
즉, input 텐서가 채널 차원이 1인 2차원 이미지 형태가 아니라면, 2차원의 kernel을 사용하는 일은 드뭅니다. 그렇기에 이 인자가 kernel 단면의 크기를 결정한다고 기술하였습니다.
stride
type: int | Tuple[int, int]
kernel은 텐서를 훑고 지나가며 연산하는데요, kernel이 데이터 상에 위치한 자리마다 하나의 값이 계산됩니다. 이때 stride는 kernel이 몇 칸씩 이동하며 계산할지를 결정합니다. int 값을 준다면 가로, 세로 동일한 간격으로 연산을 진행하겠다는 의미이며, tuple 값을 준다면 지정한 가로, 세로 간격으로 움직이며 연산을 진행하겠다는 의미입니다.
padding
type: int | Tuple[int, int] | str
기본적으로 패딩은 다양한 이유로 사용됩니다. 대표적인 쓰임새는 input 텐서와 ouput 텐서의 크기를 맞춰주는 것입니다. 최근 발표되는 대부분의 신경망은 output 텐서에 input 텐서를 더하여 다음 층으로 보내주거나, 이전 층의 특징 일부를 현재 층에서의 텐서와 결합시키는 구조를 사용하기 때문입니다.
이때, padding인자는 합성곱 연산을 통해 나온 feature map에 적용할 패딩의 개수를 의미합니다. int 값을 준다면 위아래, 양옆 모두 동일한 칸만큼의 패딩을 주고, tuple 값을 준다면 첫 번째 값은 위아래 패딩을, 두 번째 값은 양옆 패딩을 결정합니다.
pytorch 1.9.0부터는 특수한 문자열 입력도 지원되는데요, “valid”는 no-padding을 의미하고, “same”은 input의 높이, 너비와 동일한 크기의 output을 내도록 padding을 조절해 줍니다.
* 다만, stride가 1이 아닌 경우에는 “same”의 입력을 지원하지 않습니다.
padding_mode
type: str
padding인자가 패딩의 크기를 정한다면, padding_mode는 어떤 값으로 패딩할지를 결정합니다. “zeros”, “reflect”, “replicate”, “circular” 4가지 모드가 지원되며, 각각에 해당하는 예시는 아래와 같습니다.
device
type: str
합성곱 신경망의 가중치 값들이 올라와 있는 위치입니다. 예를 들어 cpu가 될 수도 있고, gpu나 tpu가 될 수도 있습니다.
가끔 모델을 학습시키거나 성능 테스트를 진행하다 보면, “Expected all tensors to be on the same device” 에러가 발생합니다. 데이터가 올라가 있는 위치와 이 값이 다르다면 서로 다른 메모리에 있는 값끼리 연산해야 하기 때문에 발생하는 에러입니다.
dtype
type: class
가중치가 저장될 숫자 타입입니다. 얼마나 정밀한 계산을 할지에 따라 선택할 수 있습니다.
심화 파라미터 파헤치기
여기서부터 설명드리는 파라미터는 입문자분들에게 살짝 생소할 수 있는 부분을 결정합니다. 다시 말해 이 부분을 완벽히 이해하신다면, 더 이상 pytorch에서 구현한 합성곱 층에 대해서는 추가적으로 알아야 할 내용이 없다고 봐도 무방합니다.
dilation
type: int | Tuple[int, int]
기본적인 kernel 단면의 모양은 꽉 차 있는 직사각형입니다. pytorch에서는 이 상황을 단면의 각 칸 사이의 거리가 1만큼 떨어져 있다고 보며, 이때의 1이라는 값이 dilation입니다. 즉, dilation이 2가 된다면 아래 그림과 같이 kernel 단면이 마치 체스판에서의 검정색 칸의 모양이 됩니다.
groups
type: int
groups인자는 말 그대로 input 텐서를 지정된 값만큼의 그룹으로 나누어 합성곱 연산을 진행하도록 해줍니다. 예시와 함께 살펴보도록 하겠습니다.
아래 그림과 같이 input 텐서의 차원이 (6, 64, 64)이고, 보고자 하는 합성곱 층의 out_channels가 10, kernel_size가 3, stride가 1, padding도 1이라 가정합시다. 텐서의 차원상, in_channels는 6으로 설정되어야 하고, dilation은 1로 가정합니다.
기본적인 합성곱 연산은 (6, 3, 3) 형태의 kernel이 10개 존재하여, 각 kernel이 (64, 64) 형태의 feature map 10개를 만듭니다. 즉, 우리가 기본적인 합성곱 층에서 학습하고자 하는 것은 10개의 (6,3,3) 형태의 kernel입니다.
만약, 같은 상황에서 groups 인자를 2로 주면 어떨까요?
데이터는 마치 (3, 64, 64) 두개로 나뉜 것처럼 동작합니다. kernel은 (3, 3, 3) 형태 10개가 되어 처음 5개의 kernel은 나뉜 데이터 중 첫 번째에 적용되어 (5, 64, 64)의 feature map을, 나중 5개의 kernel이 나뉜 데이터 중 두 번째에 적용되어 나머지 (5, 64, 64)의 feature map을 만듭니다. 결과적으로 두 feature map을 이어붙여 (10, 64, 64)의 feature map을 output으로 내놓습니다. 따라서 여기서 학습하고자 하는 것은 10개의 (3, 3, 3) 형태의 kernel입니다.
정리하자면, groups 인자로 주는 값만큼 input 텐서를 채널축에 수직하게 split 하고, 각 그룹에 따로 합성곱 연산을 적용하는 것입니다. 그렇기 때문에 input 텐서의 채널 차원도 groups로 나눠져야 하고, output 텐서의 채널 차원도 groups로 나눠져야 합니다. 따라서 groups 인자로 줄 수 있는 값은 input 텐서의 채널 차원과 output 텐서의 채널 차원의 공약수로 제한됩니다.
* 이러한 방식으로 진행하는 합성곱 연산을 depth-wise 합성곱 연산이라고 부르기도 합니다.
bias
type: bool
각 합성곱 연산마다 학습 가능한 상수를 더해줄지 말지를 결정합니다. 이 값이 false라면 단순히 합성곱 연산의 결과가 output 텐서가 되며, true라면 이 값에 추가적인 상수를 더한 값이 output 텐서가 됩니다.
수학적인 관점에서 둘은 각각 linear transformation과 affine transformation입니다. 후자가 같은 차원의 벡터를 가정했을 때에 더 많은 자유도를 가지게 됩니다.
글을 마치며
지금까지 2차원 합성곱 층의 개념과 이것의 구현체인 torch.nn.Conv2d에서 지원하는 다양한 parameter의 역할을 살펴보았습니다.
함수를 무작정 사용하는 것보다는 함수가 지원하는 옵션을 이해하고 사용하는 것이, 그것보다도 좋은 습관은 이 함수가 생겨난 배경과 개념을 알고 사용하는 것입니다. 이는 탄탄한 기본기를 만들어줄 뿐만 아니라 장기적으로는 높은 생산성까지 이끌어내줄 수 있으리라 생각합니다. 독자분들께서 이 글을 통해 탄탄한 기본기를 쌓아가셨기 바랍니다.
참고 문헌
액션파워에 대한 더 많은 이야기가 궁금하다면?
More about Action Power