이미지를 잘 보려면? Convolution!

안녕하세요! Perfitt AI team의 신입 머신러닝 엔지니어 Fisher 입니다. Perfitt 발 분석 AI 프로세스를 배우기 위해 이번 글에서는 이미지 처리의 기본인 Convolution에 대해 말해보려 합니다. Convolution이 어떻게 이미지를 처리하는지 그리고 Standard Convolution에서 어떤 부분을 개선하려고 새로운 Convolution 기법들이 나왔는지 알아보도록 하겠습니다.

목적

  • 발 이미지 Segmentation에 대한 이해와 이미지 분석을 위한 기초 지식 및 코드 구현의 필요성
    단순히 TensorFlow를 이용해서 구현하는 것이 아닌 NumPy로 Low Level부터 구현을 통해서 다양한 Convolution을 이해하고 추후 Fine Tuning 및 다양한 작업을 할 수 있도록 하는 것이 중요합니다.

Convolution?

기존 Multi-Layer Neural Network 문제점

Parameter 개수는 25600(w1,1,...w256,100w_{1,1}, ...w_{256,100}

그리고 이미지가 회전되거나 잘리는 부분이 있거나 하면 결과가 크게 차이날 수 있습니다.

이미지를 Fully Connected Multi-Layer Neural Network로 처리하면 아래와 같은 세가지 문제가 발생합니다.

  1. 학습 시간
  2. 모델의 크기
  3. 파라미터 개수

이러한 문제를 해결하기 위해서 Convolution Neural Network(CNN)이 제안되었습니다.

Convolution

Convolution은 주로 Filter 연산에 사용됩니다. Image에서 Feature map을 추출하기 위해 사용되며, 보통 3x3으로 Window를 이동하여 Element-wise로 곱해주며 새로운 Feature map을 만들어 줍니다. 그리고 CNN은 아래와 같은 특징을 갖고 있습니다.

Locality

CNN은 Local 정보를 활용합니다. 공간적으로 가까운 신호들에 대해서 Correlation 관계를 비선형 필터를 적용해서 추출해냅니다. 필터를 여러개 적용하면 다양한 Local 특징을 추출해낼 수 있게 됩니다. 그리고 Subsampling 과정을 통해서 이미지의 크기를 줄이고 Local feature들에 대한 Filter 연산을 반복적으로 수행해 Global feature를 얻을 수 있도록 합니다.

Shared Weights

필터들을 이미지에 반복적으로 적용해서 파라미터 수를 줄이고 이미지가 잘리거나 회전 된 거 같은 Topology 변화에 Robust 하도록 만들어 줍니다.

CNN

CNN은 크게 3가지 단계로 구성됩니다.

  1. Feature를 추출하기 위한 단계
  2. Topology 변화에 Robust 하도록 해주는 단계
  3. 분류 단계

CNN은 Feature를 추출하는 단계가 포함되어 있어서 RAW Image에 대한 연산이 가능하고 그렇기 때문에 기존에 알고리즘들과 다르게 전처리 과정을 필요로 하지 않습니다.

Filter와 Subsampling 과정을 반복적으로 수행하면서 Local feature로부터 Global feature를 얻어냅니다. 분류 단계에서는 기존의 신경망들과 동일한 구조를 통해서 분류 작업을 합니다.

대부분의 Image 처리 알고리즘에서는 Feature를 추출하기 위해서 Filter를 사용합니다. 보통 필터는 3x3, 5x5의 크기로 사용하고 작은 영역(Receptive field)에 대해 적용됩니다. CNN에서 Filter, Convolutional layer는 학습을 통해서 최적의 파라미터를 찾아가는 점이 특징입니다.

기존의 Subsampling은 보통 고정된 위치에 있는 픽셀을 고르거나 윈도우 안에 있는 픽셀들의 평균값으로 대체하는 방식을 많이 사용했습니다. CNN에서는 보통 강한 특징을 갖고 있다고 볼 수 있는 특징만 활용하는 Max pooling을 통해서 Subsampling을 진행합니다.

이미지 변화(위치, 회전 등)에 Robust 해지려면 더 강하고 Global한 Feature를 추출해야 합니다. 보통 Convolution + Subsampling 과정을 여러번 거치게 된다면 이미지를 대표한다고 할 수 있는 Global한 특징을 가지게 됩니다. 이렇게 얻은 Feature를 Fully Connected Network를 통과시켜서 학습을 진행하면 Receptive filed와 Global feature을 특성을 모두 살려서 이미지를 잘 처리할 수 있게 됩니다.

여러개의 Feature map을 추출하고 싶다면 Convolution Kernel 파라미터를 설정해주면 됩니다. 다음으로 Subsampling을 진행하여 Feature map의 크기를 줄이고 Robust 하게 만듭니다. 일반적으로는 1개의 Feature map에 대해 1개의 Subsampling을 수행합니다. 이렇게 해서 Local feature를 얻으면 다시 Convolution + Subsampling을 진행해서 조금 더 나은 Global feature를 얻도록 합니다.
여러 Layer(Convolution + Subsampling)을 통과하면 Feature map의 크기가 작아지면서 더 Global한 특징들만 남게 됩니다. 이렇게 구한 Global feature들은 Fully Connected Network의 입력으로 연결이 되며 일반적인 신경망과 같은 과정으로 학습을 하게 됩니다. 즉, CNN의 주요 Layer는 Convolution Layer, Pooling(Subsampling) Layer, Fully Connected Layer 입니다.

Hyper parameter for CNN

Filter의 개수

Feature map의 크기는 Layer의 Depth가 커질수록 작아지기 때문에 일반적으로 앞쪽 Layer는 Filter의 개수가 적고 뒤쪽으로 갈수록 Filter의 개수가 증가하는 편입니다.

Tc=Np×Nf×TkTc = Np \times Nf \times Tk

NpNp: 출력 Pixel 수, Nf:Nf: 전체 Feature map 개수, Tk:Tk: Filter당 연산 시간

Convolution Layer의 연산 시간은 위와 같이 계산된다. 필터의 개수를 정할 때 많이 사용하는 방법은 각 Layer마다 연산 시간을 비슷하게 유지하도록 조정하는 것입니다. 보통 Pooling layer에서 2x2 Subsampling을 진행하는데 Convolution layer를 지날 때마다 Pixel의 수가 1/4로 감소하기 때문에 Feature map의 개수를 4배 정도 증가시키면 연산 시간이 비슷할 것이라고 생각할 수 있습니다. Feature map의 개수는 Weight와 Bias같은 Parameter 개수를 결정하는 요소이고 데이터와 Task에 따라서 결정하게 됩니다.

Filter의 형태

필터의 형태는 Task마다 다르며 데이터에 따라서 적절하게 선택하면 됩니다. 일반적으로 작은 크기(28x28)의 이미지에 대해서는 5x5 필터를 주로 사용하지만 큰 크기의 이미지의 경우는 첫 단계에서 15x15같은 큰 크기의 필터를 사용하기도 합니다. 큰 크기의 필터를 하나 사용하는 것 보다는 작은 크기의 필터를 여러번 사용하는 것이 효과적입니다. 여러개의 작은 필터들을 사용하면 비선형성을 활용하여 Feature를 더 잘 추출할 수 있습니다. 그리고 여러개의 필터를 중첩해서 사용하는게 연산량도 더 적게 만드는 효과가 있습니다.

Stride Value

Stride는 Convolution을 수행할 때 건너 뛸 픽셀의 개수를 결정합니다. Stride는 Input image의 크기가 큰 경우 연산량을 줄이기 위해서 앞쪽 Layer에 적용합니다. Stride를 1로 하면 모든 Input Image Pixel에 대해서 Convolution 연산을 수행하고 Pooling을 하면서 좋은 Feature를 선택할 수 있지만 Stride를 크게 한다면 좋은 Feature를 건너뛰게 할 수도 있습니다. 그래서 보통 Stride를 1로하고 Pooling을 통해서 Subsampling을 하는게 결과가 좋은 편입니다.

Zero Padding

보통 Convolution 연산을 하게되면 경계 처리 문제때문에 Feature map의 크기가 Input image의 크기 보다 작아지게 됩니다. Zero Padding은 크기를 맞춰주기 위해서 테두리에 0을 채워주는 것이다. 28x28 이미지에 0을 2픽셀씩 추가하면 30x30이미지가 되고 3x3 필터를 사용하면 28x28의 Feature map이 Output으로 나오게 됩니다. 이렇게 Input size와 Output size를 Padding을 통해서 맞춰줄 수 있다. 그리고 Zero Padding을 하게 되면 경계(테두리) 부분의 정보까지 더 잘 활용할 수 있어서 보통 Zero Padding을 사용하는 게 결과가 더 좋은 편입니다.

Convolution Layer는 이미지의 Feature를 추출하는 역할을 합니다. Filter 행렬이 이미지를 처음부터 끝까지 훑으면서 필터의 크기에 해당하는 이미지의 원소들을 필터의 원소들과 Element-Wise로 곱해줍니다.

필터는 보통 1칸씩 이동하지만 이동하는 범위를 지정해 줄 수 있는데 이것을 Stride라고 합니다. Stride가 2인 경우는 아래 그림과 같이 필터가 이동합니다.

Padding

Convolution을 하게 되면 원래 이미지 크기보다 작아지게 되는데 여러 층을 쌓는다면 최종 Feature map은 원래 Input 이미지보다 매우 작아지게 됩니다. 그렇게 되면서 이미지의 가장 자리에 있는 정보들이 손실될 수 있는데, 이러한 것을 방지하기 위해서 Padding을 사용해서 테두리를 채워 주면 Feature map의 크기를 유지할 수 있게 됩니다. Padding은 Convolution 연산 전에 Input의 가장자리에 지정된 개수만큼 행과 열을 추가해 줄 수 있습니다. 주로 Zero Padding을 사용하며 Input shape과 Output shape을 맞춰주기 위해서 많이 사용합니다.

Feature map Size

Oh=floor(IhKh+2PS+1),Ow=floor(IwKw+2PS+1)O_h = floor({{I_h - K_h +2P} \over S} +1), \, O_w = floor({{I_w - K_w+2P} \over S} + 1)

Ih:I_h:


3차원 데이터 Case

2차원 예제는 이해가 쉽게 되는 편이지만 3차원으로 넘어가면서 난이도가 높아지고 실제 데이터는 Channel이 존재하는 3차원의 데이터가 대부분일 것이기 때문에 정확한 이해가 필요합니다. 3차원 데이터에 대해서 Convolution 계산을 실행할 때 Input과 Filter의 Channel 수가 같아야 합니다.

3차원 데이터 Shape은 (Height, Width, Channel)로 표현합니다.

3차원 Input 데이터에 하나의 필터를 사용해서 Convolution 계산을 하면 Output으로 하나의 Channel만 가지는 Feature map이 생성됩니다. Output이 여러 개의 Channel을 갖게 하기 위해서는 여러 개의 필터를 사용하면 됩니다. N개의 필터를 사용하면 N개의 채널을 가지는 Feature map을 Output으로 생성합니다.


NumPy로 convolution 구현하기

def Conv2D(image, out_channels, kernel, padding=0, stride=1):
    '''
    :param image: (C, H, W)
    :param out_channel: (out_channels, output_height, output_width)
    :param kernel: (k, k)
    :param padding: default 1
    :param stride: default 1
    :return: output(feature map)
    '''

    image_channel, image_height, image_width = image.shape[0], image.shape[1], image.shape[2]

    kernel_channel, kernel_height, kernel_width = image_channel, kernel[0], kernel[1]
    kernel = np.random.random((image_channel, kernel_height, kernel_width))

    output_height = int(((image_height - kernel_height + 2 * padding) / stride) + 1)
    output_width = int(((image_width - kernel_width + 2 * padding) / stride) + 1)
    output_channel = out_channels

    output = np.zeros((output_channel, output_height, output_width))

    if padding != 0 :
        imagePadded = np.zeros((image_channel, image_height + padding * 2, image_width + padding * 2))
        imagePadded[:, padding:(-1 * padding), padding:(-1 * padding)] = image

    else:
        imagePadded = image

    for z in range(0, output_channel):
        output_per_channel = np.zeros((output_height, output_width))

        for y in range(0, output_height):
            if (y * stride + kernel_height) <= imagePadded.shape[1]:

                for x in range(0, output_width):
                    if (x * stride + kernel_width) <= imagePadded.shape[2]:
                        output_per_channel[y][x] = np.sum(imagePadded[:,
                                                               y*stride : y*stride + kernel_height,
                                                               x*stride : x*stride + kernel_width] * kernel).astype(np.float32)


        output[z, :, :] = output_per_channel

    return output

Code

tf.keras.layers.Conv2D

# The inputs are 28x28 RGB images with `channels_last` and the batch
# size is 4.
input_shape = (4, 28, 28, 3) # num, h, w, c
x = tf.random.normal(input_shape)
y = tf.keras.layers.Conv2D(
2, 3, activation='relu', input_shape=input_shape[1:])(x)
print(y.shape)
(4, 26, 26, 2)
  • filters = Integer, the dimensionality of the output space (i.e. the number of output filters in the convolution).
  • kernel_size = An integer or tuple/list of 2 integers, specifying the height and width of the 2D convolution window. Can be a single integer to specify the same value for all spatial dimensions.
  • dilation_rate: an integer or tuple/list of 2 integers, specifying the dilation rate to use for dilated convolution. Can be a single integer to specify the same value for all spatial dimensions. Currently, specifying any dilation_rate value != 1 is incompatible with specifying any stride
    value != 1.
  • Kernel(filter)의 Element value들은 initialize 값들에 따라서 들어가는 것이고 Xaiver init을 주로 많이 사용합니다. kernel_initializer='glorot_uniform'

Depthwise Separable Convolution

Depthwise는 RGB 채널별로 각각 나눠서 Convolution을 진행해서 계산량을 줄입니다.

기존의 Convolution은 R, G, B 각각 Feature map을 뽑고서 다 더 해줘서 Feature map을 만듭니다. Depthwise의 경우 Filter를 통과한 R, G, B가 다른 연산 없이 그대로 Feature map이 됩니다.

기존의 Convolution은 feature map = kernel size * kernel size * 3, Depthwise의 경우는 feature map = kernel size * kernel size

Depthwise Convolution 계산량이 Input channel만큼 줄어 듭니다.

  1. Input을 각각의 채널로 나눈다
  2. 각 Input을 Depthwise kernel이라고 부르는 커널과 곱해준다
  3. 각각의 Output을 채널별로 쌓는다.
self.depthwise = tf.keras.layers.DepthwiseConv2D(kernel_size=3, use_bias=False, strides=(stride, stride), padding='same', dilation_rate=(atrous_rate, atrous_rate), name='depthwise')

Separable Convolution

Pointwise Convolution이라고도 부르기도 합니다. 1x1 conv로서 공간적인 특성을 가지고 있지 않으며 계산량이 적어서 Feature map의 크기를 조절할 때 사용합니다. Conv2D 에서 kernel_size를 1로 설정해주면 됩니다.

self.pointwise = tf.keras.layers.Conv2D(filters=channels, kernel_size=1, use_bias=False, padding='same', dilation_rate=(1, 1), name='project')

마치며

이번 글에서는 Standard Convolution과 Depthwise Separable Convolution에 대해서 알아봤습니다. 여러 Convolution Network에서 많이 사용하는 Convolution 기법들을 알아봤는데 여기에 있는 기법외에도 여러가지 Convolution 기법들이 있습니다. 하지만 Standard Convolution에 대해서 정확히 이해하고 있으면 새로운 기법을 공부하더라도 쉽고 빠르게 이해할 수 있을 것입니다. 눈으로만 보는 것이 아니라 직접 적고 코드를 실행해가면 더 쉽게 개념을 이해하고 활용할 수 있을 것입니다. 더 좋은 것은 많은 고민을 혼자 해보고 잘 아는 동료에게 질문하는 것이겠죠. 아직 부족하지만 앞으로 많은 논문, 기법들을 소개하고 공유하는 글을 작성해보도록 하겠습니다.

Reference

좋은 웹페이지 즐겨찾기