Convolution 연산을 Python NumPy로 구현해보자! (단순 반복문을 이용한 구현, im2col을 이용한 구현)
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.]]]]
'기타' 카테고리의 다른 글
크롬(Chrome) 브라우저 시크릿 모드 사용 방법 (+ 간단한 원리 설명) (0) | 2021.08.28 |
---|---|
인터넷 속도(다운로드, 업로드, Ping) 간단 측정 방법! Feat. NIA 사이트 (0) | 2021.08.27 |
윈도우(Windows) 10에서 기본 브라우저 변경하는 방법 (0) | 2021.08.27 |
Rufus를 이용해 윈도우 10 OS 이미지를 USB에 굽는 방법 (0) | 2021.08.26 |
텍스트를 음성(오디오)으로 바꾸어 주는 TTS 프로그램 발라볼카(Balabolka) 소개 (0) | 2021.08.26 |