[딥러닝 Express] 09. 컨벌루션 신경망

 

1. 컨벌루션 신경망 소개

CNN 기본 구조

컨벌루션 신경망은 합성곱, Convolution Neural Network (CNN) 으로 불리며 그림과 같이 입력 데이터의 부분적으로만 연결되어 학습이 진행된다. 지금까지 다루었던 신경망에서는 하위 Layer의 유닛들과 상위 Layer의 유닛들이 완전히 연결되어 있었다. 신경망에 매개변수(가중치)가 너무 많으면 과잉 적합에 빠질 수도 있고 학습이 늦어진다. 따라서 CNN은 이미지처리에서 복잡도가 낮아지고 과잉적합에 빠지지 않는다.

이미지의 종류를 판별하거나 이미지 안의 물체를 추적하거나, 숫자나 얼굴 인식, 이미지의 내용을 이해하여 이미지를 분류하거나 주석을 붙이는 작업 등 현업에서 여러 방식으로 사용되고 있다.

 

 

2. 컨벌루션 연산

ResNet Architecture

CNN은 보통 이미지의 특징을 뽑아내는 body 영역, 특징을 이용한 downstream task를 수행하기 위한 head 부분으로 구분할 수 있다. Body 영역은 backbone으로도 불리며 ResNet이 대표적이다. 과거에는 convolution 연산마다 pooling 연산을 수행하였는데, 최근에는 연산의 시작과 종료 혹은 종료시점에서만 사용한다.

 

 

합성곱 연산

 

3차원 텐서의 합성곱 연산

Convolution 연산은 위 그림과 같이 입력과 각 Convolution 커널과의 곱연산, 그 이후 합연산을 통해 특징을 생성한다. 여기서 3차원 등 다차원일 경우에는 연산한 특징을 다시 한번 합연산을 수행하여 최종 특징을 생성한다.

 

 

이미지 패딩

Convolution 연산을 수행할 때 패딩을 추가하는 경우가 있다. 패딩의 값은 임의로 지정할 수 있지만 대부분 0으로 지정하고 이를 제로패딩이라고 부른다. 보통 CNN 모델을 보면 입력 데이터는 layer를 통과하면서 점점 작아지는 모습을 볼 수 있는데, 패딩을 적절히 추가하면 크기를 유지하거 늘리는 방식으로 사용할 수 있다. 이러한 기법은 Super Resolution 분야에서 대표적으로 활용하고 있다.

 

 

3. 풀링(서브 샘플링)

Pooling

Convolution Layer의 출력 데이터를 입력으로 받아서 출력 데이터의 크기를 줄이거나, 특정 데이터를 강조한다. 풀링 층에서는 특성 맵을 다운샘플링하여 특성 맵의 크기를 줄이는 풀링 연산이 이루어집니다. 풀링 연산에는 일반적으로 최대 풀링(max pooling)과 평균 풀링(average pooling)이 사용된다.

 

풀링 연산에서도 합성곱 연산과 마찬가지로 커널과 스트라이드의 개념을 가진다. 위의 그림은 스트라이드가 2일 때, 2 × 2 크기 커널로 맥스 풀링, 평균 연산을 했을 때 특성맵이 절반의 크기로 다운샘플링되는 것을 보여준다. 맥스 풀링은 커널과 겹치는 영역 안에서 최대값을 추출하는 방식으로 다운샘플링한다. 평균 풀링은 최대값을 추출하는 것이 아니라 평균값을 추출하는 연산이 진행된다. 풀링 연산은 커널과 스트라이드 개념이 존재한다는 점에서 합성곱 연산과 유사하지만, 합성곱 연산과의 차이점은 학습해야 할 가중치가 없으며 연산 후에 채널 수가 변하지 않는다.

 

 

4. 컨벌루션 신경망을 해석해보자

Residual 연산

기존의 문제점은 네트워크의 깊이가 깊어질수록 underfitting 문제가 발생했다. Residual Block은 그림과 같이 이전 레이어의 입력 x를 현재 레이어의 출력 F(x)에 더해주는 모듈이다. 이 개념 덕분에 층이 깊어져도 입력 정보를 유지할 수 있어서  최소한 기존 네트워크보다 성능이 떨어지지 않는 구조가 된다.

 

class Bottleneck(nn.Module):
    expansion = 4
    def __init__(self, in_channels, out_channels, i_downsample=None, stride=1):
        super(Bottleneck, self).__init__()
        
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0)
        self.batch_norm1 = nn.BatchNorm2d(out_channels)
        
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=stride, padding=1)
        self.batch_norm2 = nn.BatchNorm2d(out_channels)
        
        self.conv3 = nn.Conv2d(out_channels, out_channels*self.expansion, kernel_size=1, stride=1, padding=0)
        self.batch_norm3 = nn.BatchNorm2d(out_channels*self.expansion)
        
        self.i_downsample = i_downsample
        self.stride = stride
        self.relu = nn.ReLU()
        
    def forward(self, x):
        identity = x.clone()
        x = self.relu(self.batch_norm1(self.conv1(x)))
        
        x = self.relu(self.batch_norm2(self.conv2(x)))
        
        x = self.conv3(x)
        x = self.batch_norm3(x)
        
        #downsample if needed
        if self.i_downsample is not None:
            identity = self.i_downsample(identity)
        #add identity
        x+=identity
        x=self.relu(x)
        
        return x

 

이름이 bottleneck인 이유는 아래와 같다.

1×1 Conv (첫 번째): 채널 수를 줄여서 계산량을 확 줄인다 → 좁아짐 = 병목 구간

3×3 Conv: 줄어든 채널로 연산 수행 → 효율적

1×1 Conv (마지막): 다시 원래 채널 수로 복원

 

5. 컨벌루션 신경망 구현하기 & 6. MNIST 필기체 숫자 인식

교재 참고