안경잡이개발자

728x90
반응형

  Convolution 연산CNN을 포함한 다양한 딥러닝 네트워크에서 활용되는 중요한 연산이다. PyTorch와 같은 딥러닝 프레임워크에서는 Convolution 연산을 기본적으로 제공하고 있다. 이러한 Convolution 연산에 대해 더욱 자세히 이해하기 위하여 본 포스팅에서는 Convolution 연산을 Python NumPy만을 이용해 구현해보도록 하겠다.

 

  예를 들어 입력 차원이 (배치 크기, 채널 크기, 높이, 너비) = (2, 3, 5, 5)인 예시를 확인해 보자. 이때 3 X 3짜리 커널을 3개 사용하여 stride = 2, padding = 0 설정으로 Convolution 연산을 수행하면 그 결과는 다음과 같다. 수식으로 표현하자면 출력 차원은 (배치 크기, 커널 개수, out_h, out_w) = (2, 3, 2, 2)가 된다.

 

 

※ 단순 반복문을 이용한 구현 ※

 

  이러한 Convolution 연산을 어떻게 구현할 수 있을까? 가장 기본적인 방법은 바로 반복문을 이용하는 것이다. 기본적으로 출력 높이(out_h)와 출력 너비(out_w)는 다음의 공식을 이용해 구현할 수 있다.

out_h = (h + 2 * padding - filter_h) / stride + 1
out_w = (w + 2 * padding - filter_w) / stride + 1

 

  또한, 일반적으로 패딩(padding)은 입력 데이터의 높이와 너비 차원에 대해서만 수행한다. 이를 위해 np.pad() 메서드를 사용할 수 있다. 아래 코드를 보면 높이와 너비 차원에 대해서만 패딩을 넣는 것을 알 수 있다.

 

  결과적으로 출력 텐서(out)를 만든 뒤에 각각의 원소마다 Convolution 연산을 수행한 결과 값을 채워 넣는 것을 알 수 있다. NumPy에서 단순히 두 행렬에 대하여 곱셈 연산을 수행하면, 원소 단위(element-wise)로 곱셈 연산이 수행된다. 그래서 Convolution 연산을 다음과 같이 구현할 수 있다.

 

import numpy as np


def conv(X, filters, stride=1, pad=0):
    n, c, h, w = X.shape
    n_f, _, filter_h, filter_w = filters.shape

    out_h = (h + 2 * pad - filter_h) // stride + 1
    out_w = (w + 2 * pad - filter_w) // stride + 1

    # add padding to height and width.
    in_X = np.pad(X, [(0, 0), (0, 0), (pad, pad), (pad, pad)], 'constant')
    out = np.zeros((n, n_f, out_h, out_w))

    for i in range(n): # for each image.
        for c in range(n_f): # for each channel.
            for h in range(out_h): # slide the filter vertically.
                h_start = h * stride
                h_end = h_start + filter_h
                for w in range(out_w): # slide the filter horizontally.
                    w_start = w * stride
                    w_end = w_start + filter_w
                    # Element-wise multiplication.
                    out[i, c, h, w] = np.sum(in_X[i, :, h_start:h_end, w_start:w_end] * filters[c])

    return out

 

  이제 앞서 다루었던 그림을 그대로 코드로 옮겨 결과를 확인해 보자.

 

X = np.asarray([
# image 1
[
    [[1, 2, 9, 2, 7],
    [5, 0, 3, 1, 8],
    [4, 1, 3, 0, 6],
    [2, 5, 2, 9, 5],
    [6, 5, 1, 3, 2]],

    [[4, 5, 7, 0, 8],
    [5, 8, 5, 3, 5],
    [4, 2, 1, 6, 5],
    [7, 3, 2, 1, 0],
    [6, 1, 2, 2, 6]],

    [[3, 7, 4, 5, 0],
    [5, 4, 6, 8, 9],
    [6, 1, 9, 1, 6],
    [9, 3, 0, 2, 4],
    [1, 2, 5, 5, 2]]
],
# image 2
[
    [[7, 2, 1, 4, 2],
    [5, 4, 6, 5, 0],
    [1, 2, 4, 2, 8],
    [5, 9, 0, 5, 1],
    [7, 6, 2, 4, 6]],

    [[5, 4, 2, 5, 7],
    [6, 1, 4, 0, 5],
    [8, 9, 4, 7, 6],
    [4, 5, 5, 6, 7],
    [1, 2, 7, 4, 1]],

    [[7, 4, 8, 9, 7],
    [5, 5, 8, 1, 4],
    [3, 2, 2, 5, 2],
    [1, 0, 3, 7, 6],
    [4, 5, 4, 5, 5]]
]
])
print('Images:', X.shape)

filters = np.asarray([
# kernel 1
[
    [[1, 0, 1],
    [0, 1, 0],
    [1, 0, 1]],

    [[3, 1, 3],
    [1, 3, 1],
    [3, 1, 3]],

    [[1, 2, 1],
    [2, 2, 2],
    [1, 2, 1]]
],
# kernel 2
[
    [[5, 1, 5],
    [2, 1, 2],
    [5, 1, 5]],

    [[1, 1, 1],
    [1, 1, 1],
    [1, 1, 1]],

    [[2, 0, 2],
    [0, 2, 0],
    [2, 0, 2]],
],
# kernel 3
[
    [[5, 1, 5],
    [2, 1, 2],
    [5, 1, 5]],

    [[1, 1, 1],
    [1, 1, 1],
    [1, 1, 1]],

    [[2, 0, 2],
    [0, 2, 0],
    [2, 0, 2]],
]
])
print('Filters:', filters.shape)

out = conv(X, filters, stride=2, pad=0)
print('Output:', out.shape)
print(out)

 

  실행 결과는 다음과 같다. 그림에서의 결과와 동일한 것을 확인할 수 있다.

 

Images: (2, 3, 5, 5)
Filters: (3, 3, 3, 3)
Output: (2, 3, 2, 2)
[[[[174. 191.]
   [130. 122.]]

  [[197. 244.]
   [165. 159.]]

  [[197. 244.]
   [165. 159.]]]


 [[[168. 171.]
   [153. 185.]]

  [[188. 178.]
   [168. 200.]]

  [[188. 178.]
   [168. 200.]]]]

 

※ im2col을 이용한 구현 ※

 

  앞서 단순 반복문을 이용해 Convolution 연산을 구현해 보았다. Convolution 연산을 수행할 때 단순히 반복문(for loops)을 이용하는 경우, 행렬 곱을 제대로 활용하지 못한다는 점에서 속도가 느리다. 따라서 메모리를 조금 더 많이 사용하여, 속도를 비약적으로 개선할 수 있는 방법으로 im2col 연산을 활용하는 방법이 있다.

 

  다음과 같이 한 배치에 두 장의 이미지가 존재하여 마찬가지로 입력 차원이 (배치 크기, 채널 크기, 높이, 너비) = (2, 3, 5, 5)인 예시를 확인해 보자. 이러한 입력이 들어왔을 때, 이것을 행렬(matrix)로 형태를 변형하는 것이 im2col 연산이다. im2col 연산을 수행한 결과를 확인해 보자. (8, 27) 차원을 갖는 행렬이 생성되었다. 정확히는 (이미지 개수 X out_h X out_w, 입력 채널 개수 X kernel_h X kernel_w)의 차원을 갖는 행렬이 된다.

 

 

  im2col 연산은 다음과 같이 구현할 수 있다.

 

import numpy as np


def im2col(X, filters, stride=1, pad=0):
    n, c, h, w = X.shape
    n_f, _, filter_h, filter_w = filters.shape

    out_h = (h + 2 * pad - filter_h) // stride + 1
    out_w = (w + 2 * pad - filter_w) // stride + 1

    # add padding to height and width.
    in_X = np.pad(X, [(0, 0), (0, 0), (pad, pad), (pad, pad)], 'constant')
    out = np.zeros((n, c, filter_h, filter_w, out_h, out_w))

    for h in range(filter_h):
        h_end = h + stride * out_h
        for w in range(filter_w):
            w_end = w + stride * out_w
            out[:, :, h, w, :, :] = in_X[:, :, h:h_end:stride, w:w_end:stride]

    out = out.transpose(0, 4, 5, 1, 2, 3).reshape(n * out_h * out_w, -1)
    return out


X = np.asarray([
# image 1
[
    [[1, 2, 9, 2, 7],
    [5, 0, 3, 1, 8],
    [4, 1, 3, 0, 6],
    [2, 5, 2, 9, 5],
    [6, 5, 1, 3, 2]],

    [[4, 5, 7, 0, 8],
    [5, 8, 5, 3, 5],
    [4, 2, 1, 6, 5],
    [7, 3, 2, 1, 0],
    [6, 1, 2, 2, 6]],

    [[3, 7, 4, 5, 0],
    [5, 4, 6, 8, 9],
    [6, 1, 9, 1, 6],
    [9, 3, 0, 2, 4],
    [1, 2, 5, 5, 2]]
],
# image 2
[
    [[7, 2, 1, 4, 2],
    [5, 4, 6, 5, 0],
    [1, 2, 4, 2, 8],
    [5, 9, 0, 5, 1],
    [7, 6, 2, 4, 6]],

    [[5, 4, 2, 5, 7],
    [6, 1, 4, 0, 5],
    [8, 9, 4, 7, 6],
    [4, 5, 5, 6, 7],
    [1, 2, 7, 4, 1]],

    [[7, 4, 8, 9, 7],
    [5, 5, 8, 1, 4],
    [3, 2, 2, 5, 2],
    [1, 0, 3, 7, 6],
    [4, 5, 4, 5, 5]]
]
])
print('Images:', X.shape)

filters = np.asarray([
# kernel 1
[
    [[1, 0, 1],
    [0, 1, 0],
    [1, 0, 1]],

    [[3, 1, 3],
    [1, 3, 1],
    [3, 1, 3]],

    [[1, 2, 1],
    [2, 2, 2],
    [1, 2, 1]]
],
# kernel 2
[
    [[5, 1, 5],
    [2, 1, 2],
    [5, 1, 5]],

    [[1, 1, 1],
    [1, 1, 1],
    [1, 1, 1]],

    [[2, 0, 2],
    [0, 2, 0],
    [2, 0, 2]],
],
# kernel 3
[
    [[5, 1, 5],
    [2, 1, 2],
    [5, 1, 5]],

    [[1, 1, 1],
    [1, 1, 1],
    [1, 1, 1]],

    [[2, 0, 2],
    [0, 2, 0],
    [2, 0, 2]],
]
])
print('Filters:', filters.shape)

out = im2col(X, filters, stride=2, pad=0)
print('Output:', out.shape)
print(out)

 

  im2col 연산 예제 실행 결과는 다음과 같다.

 

Images: (2, 3, 5, 5)
Filters: (3, 3, 3, 3)
Output: (8, 27)
[[1. 2. 9. 5. 0. 3. 4. 1. 3. 4. 5. 7. 5. 8. 5. 4. 2. 1. 3. 7. 4. 5. 4. 6.
  6. 1. 9.]
 [9. 2. 7. 3. 1. 8. 3. 0. 6. 7. 0. 8. 5. 3. 5. 1. 6. 5. 4. 5. 0. 6. 8. 9.
  9. 1. 6.]
 [4. 1. 3. 2. 5. 2. 6. 5. 1. 4. 2. 1. 7. 3. 2. 6. 1. 2. 6. 1. 9. 9. 3. 0.
  1. 2. 5.]
 [3. 0. 6. 2. 9. 5. 1. 3. 2. 1. 6. 5. 2. 1. 0. 2. 2. 6. 9. 1. 6. 0. 2. 4.
  5. 5. 2.]
 [7. 2. 1. 5. 4. 6. 1. 2. 4. 5. 4. 2. 6. 1. 4. 8. 9. 4. 7. 4. 8. 5. 5. 8.
  3. 2. 2.]
 [1. 4. 2. 6. 5. 0. 4. 2. 8. 2. 5. 7. 4. 0. 5. 4. 7. 6. 8. 9. 7. 8. 1. 4.
  2. 5. 2.]
 [1. 2. 4. 5. 9. 0. 7. 6. 2. 8. 9. 4. 4. 5. 5. 1. 2. 7. 3. 2. 2. 1. 0. 3.
  4. 5. 4.]
 [4. 2. 8. 0. 5. 1. 2. 4. 6. 4. 7. 6. 5. 6. 7. 7. 4. 1. 2. 5. 2. 3. 7. 6.
  4. 5. 5.]]

 

  이렇게 구해진 im2col 결과 행렬(matrix)과 연산하기 위해 다음과 같이 커널(kernel) 또한 flatten을 진행하면 된다. 결과적으로 두 행렬(matrix)에 대하여 행렬 곱(matrix multiplication)을 수행하여 결과를 구할 수 있다.

 

 

  따라서 최종적인 코드는 다음과 같다.

 

import numpy as np


def im2col(X, filters, stride=1, pad=0):
    n, c, h, w = X.shape
    n_f, _, filter_h, filter_w = filters.shape

    out_h = (h + 2 * pad - filter_h) // stride + 1
    out_w = (w + 2 * pad - filter_w) // stride + 1

    # add padding to height and width.
    in_X = np.pad(X, [(0, 0), (0, 0), (pad, pad), (pad, pad)], 'constant')
    out = np.zeros((n, c, filter_h, filter_w, out_h, out_w))

    for h in range(filter_h):
        h_end = h + stride * out_h
        for w in range(filter_w):
            w_end = w + stride * out_w
            out[:, :, h, w, :, :] = in_X[:, :, h:h_end:stride, w:w_end:stride]

    out = out.transpose(0, 4, 5, 1, 2, 3).reshape(n * out_h * out_w, -1)
    return out


X = np.asarray([
# image 1
[
    [[1, 2, 9, 2, 7],
    [5, 0, 3, 1, 8],
    [4, 1, 3, 0, 6],
    [2, 5, 2, 9, 5],
    [6, 5, 1, 3, 2]],

    [[4, 5, 7, 0, 8],
    [5, 8, 5, 3, 5],
    [4, 2, 1, 6, 5],
    [7, 3, 2, 1, 0],
    [6, 1, 2, 2, 6]],

    [[3, 7, 4, 5, 0],
    [5, 4, 6, 8, 9],
    [6, 1, 9, 1, 6],
    [9, 3, 0, 2, 4],
    [1, 2, 5, 5, 2]]
],
# image 2
[
    [[7, 2, 1, 4, 2],
    [5, 4, 6, 5, 0],
    [1, 2, 4, 2, 8],
    [5, 9, 0, 5, 1],
    [7, 6, 2, 4, 6]],

    [[5, 4, 2, 5, 7],
    [6, 1, 4, 0, 5],
    [8, 9, 4, 7, 6],
    [4, 5, 5, 6, 7],
    [1, 2, 7, 4, 1]],

    [[7, 4, 8, 9, 7],
    [5, 5, 8, 1, 4],
    [3, 2, 2, 5, 2],
    [1, 0, 3, 7, 6],
    [4, 5, 4, 5, 5]]
]
])
print('Images:', X.shape)

filters = np.asarray([
# kernel 1
[
    [[1, 0, 1],
    [0, 1, 0],
    [1, 0, 1]],

    [[3, 1, 3],
    [1, 3, 1],
    [3, 1, 3]],

    [[1, 2, 1],
    [2, 2, 2],
    [1, 2, 1]]
],
# kernel 2
[
    [[5, 1, 5],
    [2, 1, 2],
    [5, 1, 5]],

    [[1, 1, 1],
    [1, 1, 1],
    [1, 1, 1]],

    [[2, 0, 2],
    [0, 2, 0],
    [2, 0, 2]],
],
# kernel 3
[
    [[5, 1, 5],
    [2, 1, 2],
    [5, 1, 5]],

    [[1, 1, 1],
    [1, 1, 1],
    [1, 1, 1]],

    [[2, 0, 2],
    [0, 2, 0],
    [2, 0, 2]],
]
])
print('Filters:', filters.shape)

stride = 2
pad = 0
X_col = im2col(X, filters, stride=stride, pad=pad)

n, c, h, w = X.shape
n_f, _, filter_h, filter_w = filters.shape

out_h = (h + 2 * pad - filter_h) // stride + 1
out_w = (w + 2 * pad - filter_w) // stride + 1

out = np.matmul(X_col, filters.reshape(n_f, -1).T)
out = out.reshape(n, out_h, out_w, n_f)
out = out.transpose(0, 3, 1, 2)

print('Output:', out.shape)
print(out)

 

  실행 결과는 다음과 같으며, 앞서 반복문을 이용한 구현 결과와 동일하다.

 

Images: (2, 3, 5, 5)
Filters: (3, 3, 3, 3)
Output: (2, 3, 2, 2)
[[[[174. 191.]
   [130. 122.]]

  [[197. 244.]
   [165. 159.]]

  [[197. 244.]
   [165. 159.]]]


 [[[168. 171.]
   [153. 185.]]

  [[188. 178.]
   [168. 200.]]

  [[188. 178.]
   [168. 200.]]]]
728x90
반응형