[코드 리뷰] nn.Conv2d는 사실 convolution을 하지 않는다.
code 다운로드: 📥 conv2d.ipynb 다운로드
어느 날 PyTorch 문서를 둘러보던 중 재밌는 사실을 알게 되었다.
nn.Conv2d 문서에 따르면,
“nn.Conv2d는 사실 convolution을 하지 않는다”
라는 것이다. 문서에 의하면 “where ⋆ is the valid 2D cross-correlation operator” 라고 하는데, cross-correlation와 convolution의 차이는 무엇일까?
import torch
import torch.nn as nn
nn.Conv2d는 무엇일까?
우선 이 포스트를 통해 nn.Conv2d가 무엇인지 고민해본다. 결국 nn.Conv2d는 파라미터(nn.Parameter)이다. 파라미터는 텐서지만, 텐서는 파라미터가 아니다. 둘의 차이는 무엇일까?
파라미터는 .requires_grad=True이며 model.parameters()에 자동으로 등록되는 텐서라고 생각하면 편하다. 아래 예제를 통해 알아보자.
즉 파라미터는 학습 가능한 텐서이다.
아래 코드에서는 rnd라는 변수를 (5,5) 사이즈의 표준 정규 분포를 따르는 텐서로 초기화한다.
torch.randn의 output이 애초에 tensor긴 하지만 본 예제에서는 명시적으로 표현하기 위해 torch.tensor로 감싼다.
rnd = torch.randn(5, 5)
t = torch.tensor(rnd)
t
출력값은 다음과 같다.
/tmp/ipykernel_723/1694594144.py:2: UserWarning: To copy construct from a tensor, it is recommended to use sourceTensor.detach().clone() or sourceTensor.detach().clone().requires_grad_(True), rather than torch.tensor(sourceTensor).
t = torch.tensor(rnd)
tensor([[-0.7679, 0.3281, 0.8054, 1.1955, -0.6240],
[ 0.5000, -0.1930, -0.0020, -1.0173, -0.0083],
[-0.7387, 0.9879, 0.8556, 0.8847, 0.2356],
[ 0.4543, 1.8703, 1.0567, 0.2428, 1.7751],
[ 0.5252, 0.1668, -0.9017, 0.5592, -0.6604]])
이번에는 nn.Parameter로 rnd 변수를 감싼 뒤 결과를 출력했다.당연하게도 두 값은 모두 같다.
param = nn.Parameter(rnd)
param
Parameter containing:
tensor([[-0.7679, 0.3281, 0.8054, 1.1955, -0.6240],
[ 0.5000, -0.1930, -0.0020, -1.0173, -0.0083],
[-0.7387, 0.9879, 0.8556, 0.8847, 0.2356],
[ 0.4543, 1.8703, 1.0567, 0.2428, 1.7751],
[ 0.5252, 0.1668, -0.9017, 0.5592, -0.6604]], requires_grad=True)
텐서와 파라미터의 차이는 .requires_grad가 True인지 아닌지와 nn.Module에서 자동으로 학습 가능한 파라미터로 포함하는지 아닌지의 차이가 있다.
print(t.requires_grad)
print(param.requires_grad)
False
True
class Model(nn.Module):
def __init__(self):
super().__init__()
self.w = param
self.t = t
model = Model()
list(model.parameters())
[Parameter containing:
tensor([[-0.7679, 0.3281, 0.8054, 1.1955, -0.6240],
[ 0.5000, -0.1930, -0.0020, -1.0173, -0.0083],
[-0.7387, 0.9879, 0.8556, 0.8847, 0.2356],
[ 0.4543, 1.8703, 1.0567, 0.2428, 1.7751],
[ 0.5252, 0.1668, -0.9017, 0.5592, -0.6604]], requires_grad=True)]
Linear와 Conv2d
class Model(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Conv2d(3, 128, (3, 3))
self.fc = nn.Linear(128, 10)
def forward(self, x):
x = self.conv(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
model = Model()
print(model)
print(type(model.conv.weight), model.conv.weight.shape)
print(type(model.conv.bias), model.conv.bias.shape)
print(type(model.fc.weight), model.fc.weight.shape)
print(type(model.fc.bias), model.fc.bias.shape)
Model(
(conv): Conv2d(3, 128, kernel_size=(3, 3), stride=(1, 1))
(fc): Linear(in_features=128, out_features=10, bias=True)
)
<class 'torch.nn.parameter.Parameter'> torch.Size([128, 3, 3, 3])
<class 'torch.nn.parameter.Parameter'> torch.Size([128])
<class 'torch.nn.parameter.Parameter'> torch.Size([10, 128])
<class 'torch.nn.parameter.Parameter'> torch.Size([10])
결국 nn.Conv2d의 weight는 학습 가능한 파라미터이고, 이 파라미터가 cross-correlation 연산에 사용된다.
nn.Conv2d는 사실 convolution을 하지 않는다.
잠시 신호처리의 관점에서 CNN을 바라보자 그러면 CNN이 왜 필터 혹은 커널로 불리는지 이해할 수 있다.
convolution 이란 어떤 함수 $f$와 커널이라는 함수 $g$중 한 함수를 뒤집고(flip) → 슬라이딩하면서 → 원소별 곱의 합(내적)을 하는 것인데, 이를 하게 되면 어떤 $f$와 $g$가 비슷한 방향을 가질 때 값이 커지게 된다. 이게 신호처리에서 필터링의 원리이고 이 때문에 CNN의 파라미터를 커널, 필터 등으로 부르는 것이다.
\[(f * g)(t) = \int f(\tau) \cdot g(t - \tau) \, d\tau\]근데 torch의 문서에서 볼 수 있듯이 CNN은 사실 convolution을 하지 않는다. cross-correlation과 convolition의 차이는 무엇일까? 우선, cross-correlation의 수식은 다음과 같다.
\[(f \star g)(t) = \int f(\tau) \cdot g(t + \tau) \, d\tau\]cross-correlation을 convolution 대신 사용하면 (1) 두 함수 중 하나를 굳이 뒤집을 필요가 없어서 연산이 단순해지고, (2) 커널이 입력과 같은 방향으로 슬라이딩하기 때문에 좀 더 직관적이다. CNN은 어차피 커널 값을 학습으로 찾으므로 flip 여부가 결과에 영향을 주지 않아 cross-correlation을 사용해도 무방하다.
아래는 어떤 필터들에 대하여 convolition과 cross-correlation의 결과 차이이다.

결과에서 볼 수 있듯이 결과는 크게 변하지 않고 한번 뒤집고 말고의 차이이다. 역사적으로 신호처리에서 convolution을 먼저 사용했고, CNN도 그 이름을 그대로 차용했기 때문에 관습적으로 convolution이라 부른다.