[인턴일지] 네 번째 인턴일지: CNN(Convolution Neural Network) 직접 구성하기
회사에서 간단한 사진의 Binary Classification을 도맡아 진행하고 있습니다. 기존에는 DenseNet121, ResNet50등 정확도가 높기로 유명한 모델들을 사용하고, 밑의 classifier만 우리 데이터에 맞게 변경하여 학습을 진행했습니다. 그러나, 정화도가 높지 않고, 모델이 뱉어내는 테스트 결과가 너무 불안정하여 결국 간단한 CNN모델을 직접 구성하여 학습시키기로 결정하였습니다. 그래서 이번엔 간단한 CNN 모델을 구성하는 방식에 대한 글을 쓰려고 합니다.
회사에 인턴으로 들어와 일을 하고 있기도 하고, 대학원을 갔다거나 실무 경력이 많은 것이 아니기 때문에 틀린 부분이 있을 수 있습니다. 혹시 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다:)
MLP의 한계
MLP는 다음과 같이 flatten 된 형태로 데이터가 입력됩니다.
사진데이터를 입력하면 여기서 문제가 생깁니다. 사진 내에 1을 판별하는 classification 모델을 만든다고 가정해봅시다.
위 사진 네 개는 모두 1을 나타냅니다. 이렇게 한 쪽으로 쏠려있을 수 있고, 돌아가 있거나 앞 사진들과는 다른 모양의 1로 나올 수도 있습니다. 보통 flatten을 하면, 왼쪽부터 차례대로 사진을 일렬로 펴게 됩니다. 그렇게되면 1이라는 숫자를 알아볼 수 있는 위치 데이터 등이 소실됩니다. 이 때문에 사진을 classification하는 작업에서 MLP가 굉장히 낮은 성능을 보여왔습니다.
CNN
이러한 MLP의 한계를 뛰어넘기 위해 나온 것이 CNN입니다. CNN은 사진을 그대로 받아, convolution이라는 연산을 하는 filter를 적용하여, 사진에서 특징을 추출하는 역할을 합니다.
Convolution
커널 또는 필터라고 불리는 nxm 크기 행렬로 이미지를 처음부터 끝까지 훑어보며 계산하는 것을 convolution이라고 합니다. 일반적으로 CNN에서 커널은 3x3 또는 5x5 크기로 사용합니다.
위 사진이 convolution 연산의 예시입니다. 색깔 박스가 움직이는 보폭은 변화시킬 수 있습니다.
좀 더 자세한 예시를 보겠습니다.
이런 식으로 커널을 입력 사진에 겹쳐서 곱한 후, 한 커널을 거치며 나온 값들을 모두 더해 출력값을 만드는 것을 convolution이라고 합니다. 그리고, convolution을 거쳐 나온 결과를 feature map이라고 부릅니다.
pytorch에서의 CNN
CNN을 직접 구현할 때, 보통 Convolution연산과 Maxpooling연산을 같이 하게 됩니다.
Convolution 연산
in_channel : 입력 벡터의 채널 크기
out_channel : 출력값에서 원하는 채널 크기
kernel_size : 커널(필터) 사이즈.
→ 저렇게 하나의 숫자로 써도 프로그램에선 3x3 크기로 인식합니다.
stride : kernel이 이동하는 보폭.
padding : 사진을 0으로 감싸는 정도
→ padding은 거의 대부분 상황에서 0으로 채웁니다. convolution 연산에 영향을 주지 않고 사진 크기를 맞추기 위해선 0이어야 하기 때문입니다.
Convolution layer를 쌓을 때, 주의해야 할 점이 있습니다. 앞 단의 Convolution layer에서 지정한 out_channels와 현재 단의 Convolution in_channels의 숫자가 같아야 한다는 점입니다. Convolution 연산 자체가 벡터의 innrt product(내적)계산이기 때문에 사이즈가 맞지 않으면, 무조건 오류가 생깁니다. 그래서, Convolution 클래스를 사용하여 모델 구조를 작성하는 것 중에 가장 중요한 것이, 한 layer를 지났을 때 나오는 feature map의 사이즈를 잘 알고 있어야 한다는 겁니다.
그래서, Convolution layer를 지났을 때, feature map 크기를 계산하는 방법에 대해 알아보겠습니다. feature map은 간단한 공식을 통해 계산이 가능합니다.
위 식에서 data size는 사진의 크기를 말합니다.
이제 실제로 한 번 계산을 해보겠습니다.
nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3)
다음과 같이 1차원 사진을 32개로 출력하고, kernel size는 3, padding은 없이 convolution 하는 코드를 거치고 나왔을 때, feature map크기를 계산해 보겠습니다. 참고로 제 데이터는 1x512x512 흑백 이미지입니다.
이렇게 간단하게 계산할 수 있습니다.
Maxpooling 연산
maxpooling 연산은 Convolution 연산과 비슷합니다. Convolution연산만 할 경우, feature map의 크기가 커져 학습 소요 시간이 오래걸립니다. 네트워크의 파라미터와 input size를 줄이기 위해 Pooling 연산을 많이 사용합니다. 저는 이 중에서도 max pooling을 사용하였습니다. max pooling은 필터 내에서 가장 큰 값을 선택하는 방식입니다.
2x2 filter를 사용해서 maxpooling을 했을 때 결과는 위 사진과 같이 나옵니다. max pooling도 위 Convoution 연산처럼 계산하는 방식이 있습니다.
CNN 모델 구성하기
위 수식을 통해 계산해서 나온 제 CNN모델은 다음과 같습니다.
class ClassificationModel(nn.Module):
'''CNN 직접 쌓아 만든 모델'''
def __init__(self):
super(ClassificationModel, self).__init__()
self.feature_extraction = nn.Sequential(
# [batch_size, 512,512,1] -> [batch_size, 510,510,32]
nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3),
nn.ReLU(inplace=True),
# [batch_size, 510,510,32] -> [batch_size, 255,255,32]
nn.MaxPool2d(kernel_size=2, stride=2),
nn.BatchNorm2d(32),
# [batch_size, 255,255,32] -> [batch_size, 253,253,64]
nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3),
nn.ReLU(inplace=True),
# [batch_size, 253,253,64] -> [batch_size, 126,126,64]
nn.MaxPool2d(kernel_size=2, stride=2),
nn.BatchNorm2d(64),
# [batch_size, 126,126,64] -> [batch_size, 124,124,128]
nn.Conv2d(in_channels=64, out_channels=128,kernel_size=3),
nn.ReLU(inplace=True),
# [batch_size, 124,124,128] -> [batch_size, 62,62,128]
nn.MaxPool2d(kernel_size=2, stride=2),
nn.BatchNorm2d(128),
)
self.classifier = nn.Sequential(
# nn.Linear(in_features=(8*6*6)*4+2, out_features= 64),
nn.Linear(in_features=(62*62*128)*4+2, out_features= 64),
nn.ReLU(inplace=True),
nn.Linear(in_features=64, out_features= 32),
nn.ReLU(inplace=True),
nn.Linear(in_features=32, out_features= 8),
nn.ReLU(inplace=True),
nn.Linear(in_features=8, out_features=1),
nn.Sigmoid()
)
사실 이 CNN의 계산 과정이 가장 필요한 이유는, classifier에서 Linear 함수의 첫 input 사이즈를 맞춰야 하기 때문입니다. 위에 주석처리를 하고 계산한 사이즈를 써주면 좀 더 쉽게 볼 수 있겠죠?